Polymorphisme avec héritage

L'auteur de ce sujet a trouvé une solution à son problème.
Auteur du sujet

Bonjour,

Je suis en train d'apprendre le C++ et ayant pratiqué principalement des langages avec un typage plus faible, il y a des trucs que je n'arrive pas à conceptualiser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class A {};
class B : public A {};
class C : public A {};

int f(C) { return 19; }
int f(B) { return 18; }

int main() {
    A a;
    std::cout << f(a);
    return 0;
}

error: no matching function for call to ‘f(A&)’

Je comprends bien pourquoi le code plante, mais je ne sais pas comment faire pour corriger ce problème.

Merci d'avance.

+1 -0
Staff

Mes connaissances en C++ sont assez limitées, mais là, je ne vois pas trop ce que tu veux faire :

  • Soit tu veut que f prenne un A, et alors il faut définir f(A).
  • Soit tu veux que utiliser l'un des f définis, et il faut que tu donnes à f soit un B, soit un C.

Dans l'autre sens, ça ne me choquerai pas ; le code suivant compile :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream> 
class A {};
class B : public A {};
class C : public A {};

int f(A) { return 19; }

int main() {
    B b;
    C c;
    std::cout << f(b);
    std::cout << f(c);
    return 0;
}

et renvoie bien 1919.

Édit : grillé.

Édité par Gabbro

Hier, dans le parc, j'ai vu une petite vieille entourée de dinosaures aviens. Je donne pas cher de sa peau.

+0 -0
Auteur du sujet

Oui, ça j'ai compris. J'ai mal expliqué mon problème.

L'idée c'est de faire quelque chose comme ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class A {};
class B : public A {};
class C : public A {};

int f(C) { return 19; }
int f(B) { return 18; }

int main() {
    std::vector<A> arr { B(), C(), B(), C() };
    for(A a : arr) {
        std::cout << f(a);
    }
    return 0;
}
+0 -0

Salut.

Pour commencer, dans le code que tu présente avec le vector de A, tu provoque un phénomène nommé slicing, c'est à dire que les objets de type B et C que tu construis deviennent de type A lorsqu'ils sont stockés dans le vecteur. Ce que tu veux est une collection hétérogène, qui est possible avec un std::vector<std::unique_ptr<A>> par exemple. De cette manière, les objets conserveront leur type original grâce au pointeur (cela fonctionne aussi avec une référence).

Pour ce que tu veux faire avec f, ce n'est pas possible tel quel en C++, le polymorphisme dynamique sur les paramètres de fonctions n'étant pas supporté. Il faudra utiliser une autre méthode, par exemple des fonctions virtuelles dans A, B et C.

Mon Github | Mantra : Un Entity-System en C++

+3 -0

Lu'!

Ce serait pas mal d'exprimer ton besoin réel. Parce que si on regarde ton code d'un point de vue typage statique. Si j'écris :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using ptr_a = std::unique_ptr<A>;

int f(C) { return 19; }
int f(B) { return 18; }

int main() {
    std::vector<ptr_a> arr { 
      make_unique<B>(), 
      make_unique<C>(),
      make_unique<A>()
    };
    for(ptr_a const& a : arr) {
        std::cout << f(*a);
    }
    return 0;
}
  • Quel est le type de f ?
  • A supposer que le typeur nous laisse faire, que se passe-t-il quand j'appelle f sur le troisième élément ?

Donc qu'essaies tu de faire et pourquoi arrives tu à cette solution ?

Sinon petite note : tes classes A, B et C devraient être non-copiable, la sémantique de valeur n'étant pas compatible avec la hiérarchie de classes (sémantique d'entité, forme orthodoxe).

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+2 -0

Je crois que ça peut se gérer avec le pattern Visitor, non ?

ρττ

C'est possible, mais le Visitor est plus adapte lorsque l'on a une lise d'objet dont on veut un comportement dynamique qui depend du type de l'objet et du type de l'appellant (double dispatching).

Quand le comportement depend uniquement du type de l'objet, c'est du polymorphisme basique, une fonction virtuelle est suffisante.

+0 -0
Staff

Pour moi dans le Visitor il y a aussi la notion d'arborescence (arbitrairement profonde) à traverser pour la traiter intégralement. Sans cette notion de "gros traitement" polymorphe, il me semble un peu overkill.

I was a llama before it was cool

+0 -0

Ce ne serait pas quelque chose comme ça que tu veux faire ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <memory>
#include <vector>

class A {
public:
  virtual int f() = 0;
};

class B : public A {
public:
  int f() {
    return 0;
  }
};

class C : public A {
public:
  int f() {
    return 1;
  }
};

int main() {
  std::vector<std::shared_ptr<A>> vec{
    std::make_shared<B>(),
    std::make_shared<C>(),
    std::make_shared<B>(),
    std::make_shared<C>()
  };

  for(std::shared_ptr<A> &elem : vec) {
    std::cout << elem->f() << std::endl;
  }

  return 0;
}

Niveau explication, voici comment le code fonctionne :

  • On déclare une classe A avec une méthode virtuelle pure. On doit déclarer cette méthode virtuelle pour que l'appelle de f se fasse en fonction du type "réel" et non pas du type déclaré. Le "pure" veut juste dire que l'on ne donne pas la corps de la fonction, ce qui rend la classe A abstraite et donc non instanciable.
  • On déclare les classes B et C. Ici, rien de particulier.
  • On déclare un vecteur de shared_ptr vers des classes héritant de A. Comme dit précédemment, on ne peut pas utiliser directement les objets, parce qu'ils sont susceptibles d'avoir des tailles différentes, ce qui pose de sérieux soucis. On n'utilise pas non plus de unique_ptr parce que ces derniers ne permettent pas d'appeler les bons destructeurs (cf. StackOverflow : Virtual destructor with virtual members in C++11).
  • On fait une boucle sur les éléments de notre vecteur
  • Les objets seront détruits et désalloués lorsque notre vecteur le sera, c'est-à-dire à la fin de la fonction main, il n'y a donc rien d'autre à faire (merci le compilateur).

Je ne suis pas vraiment expert sur l'OO en C++, donc n'hésitez pas à me corriger si j'ai dis des bêtises.

Édité par Berdes

+0 -0

On n'utilise pas non plus de unique_ptr parce que ces derniers ne permettent pas d'appeler les bons destructeurs (cf. StackOverflow : Virtual destructor with virtual members in C++11).

Berdes

Il suffit de déclarer les destructeurs comme étant virtuels. Je ne suis absolument pas d'accord avec la justification avancée dans la réponse sur SO, qui privilège une soit-disant simplicité par rapport à un comportement bien défini dans tous les cas d'application.

D'autant plus que la loi de Murphy nous dit qu'une classe destinée à être utilisée dans un contexte polymorphique, mais sans destructeur virtuel et avec un hack pour faire fonctionner tout ça, finira forcément par être utilisée sans le hack. Et là, kaboom.

Mon Github | Mantra : Un Entity-System en C++

+3 -0
  • override + const (chipotage)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class A {
public:
  virtual int f() const = 0;
};

class B : public A {
public:
  int f() const override {
    return 0;
  }
};

class C : public A {
public:
  int f() const override {
    return 1;
  }
};

On déclare un vecteur de shared_ptr vers des classes héritant de A. Comme dit précédemment, on ne peut pas utiliser directement les objets, parce qu'ils sont susceptibles d'avoir des tailles différentes, ce qui pose de sérieux soucis. On n'utilise pas non plus de unique_ptr parce que ces derniers ne permettent pas d'appeler les bons destructeurs (cf. StackOverflow : Virtual destructor with virtual members in C++11).

Je rejoins Praetonus sur ce point : définir une classe et comment elle devra être utiliser est très casse gueule.

La regle des 5/0 ne s'appliquent que lorsque l'on définit (explicite define) un comportement particulier dans l'une des fonctions concernées. Utiliser default (= implicite defined, explicite declared) ne nécessite pas de redéfinir les autres fonctions, donc on respecte la règle du 0.

De même pour la move et la copy, l'implicite defined ne bloque pas leur génération automatique.

En termes de coût, shared_ptr va devoir conserver un deleter dans le control bloc (~ 1 pointeur), un destructeur virtuel idem (pour la vtable). Donc coût identique sur ce point.

Par contre shared_ptr va ajouter également d'autres données dans le control bloc, en particulier 2 compteurs atomiques (ou 1 ? Bonne question, je ne sais pas si le compteur pour les weak est atomique ou non). Bref, surcoût important.

Et j'ajouterais que cela donne une sémantique ("cet objet possède plusieurs propriétaires") qui n'est pas vrai (il n'y a pas d'ownership partagé).

Donc très mauvaise pratique à mon sens.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <memory>
#include <vector>

class A {
public:
  virtual ~A() = default;
  virtual int f() const = 0;
};

class B : public A {
public:
  int f() const override {
    return 0;
  }
};

class C : public A {
public:
  int f() const override {
    return 1;
  }
};

int main() {
  std::vector<std::unique_ptr<A>> vec { //j'ai un doute sur cette syntaxe
    std::make_unique<B>(),
    std::make_unique<C>(),
    std::make_unique<B>(),
    std::make_unique<C>()
  };

  for(auto& elem: vec) {
    std::cout << elem->f() << std::endl;
  }

  return 0;
}

Édité par gbdivers

+0 -0

Je rejoints également @Praetonus et @gbdivers, et je trouve ce hack autour de shared au mieux douteux. Parce que le code en question ne passera pas deux maintenances sans que quelqu'un pète les verrous en question. Bref, ça pue ce fonctionnement.

First : Always RTFM - "Tout devrait être rendu aussi simple que possible, mais pas plus." A.Einstein

+0 -0
Auteur du sujet

Merci à tous pour vos réponses, et désolé pour cette réponse tardive. Par contre, doit avouer que je n'ai pas compris tout ce qui à été dit dans ce thread :p .

Ce code fait ce que je veux.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class A {
public:
    virtual int f() = 0;
};

class B : public A {
public:
    virtual int f() { return 18; }
};

class C : public A {
public:
    virtual int f() { return 19; }
};

vector<A*> arr { new B(), new C(), new B() };

for (A* a : arr) {
    cout << a->f();
}

Du coup, je ne comprends pas l'utilité d'utiliser shared_ptr<A> ou unique_ptr<A> à la place de A*.

+0 -0

Du coup, je ne comprends pas l'utilité d'utiliser shared_ptr<A> ou unique_ptr<A> à la place de A*.

Tu triches, tu n'as pas écrit une fonction main complète et correcte (ie qui gère correctement les erreurs). Essaies de proposer une version de main qui est correcte, tu verras que ce n'est pas si simple avec des pointeurs nus.

+0 -0
Auteur du sujet

Je ne comprends pas ce que tu veux dire. Mon main compile et s’exécute sans erreurs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>

class A {
public:
    virtual int f() = 0;
};

class B : public A {
public:
    virtual int f() { return 18; }
};

class C : public A {
public:
    virtual int f() { return 19; }
};

int main() {
    using namespace std;
    vector<A*> arr { new B(), new C(), new B() };
    for (A* a : arr) {
        cout << a->f();
    }
    return 0;
}

g++ -Wall -Wextra -std=c++11 test.cpp -o test

+0 -0

Pourquoi vouloir utiliser shared_ptr alors que unique_ptr appelle bien les bons destructeurs si ils sont virtuels ?

Le code suivant est bon et donne bien "Destructeur de B" puis "Destructeur de A" :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Example program
#include <iostream>
#include <memory>
#include <string>

class A
{
public:
    virtual ~A()
    {
        std::cout << "A destructor !" << std::endl;
    }
};

class B : public A
{
public:
    virtual ~B()
    {
        std::cout << "B destructor !" << std::endl;
    }
};

int main()
{
    std::unique_ptr<A> a = std::make_unique<B>();
}

@Umbra : tu oublies de delete les pointeurs…

Édité par victorlevasseur

+0 -0

@Umbra

Je pensais que tu allais au moins mettre les delete. Même pas. Tu ne sais pas que pour chaque new, tu dois en général mettre un delete (ou delete[] si tu as appelé new[]).

Mais le piège n'est même pas là. Pense à comment faire un code correct, si une exception est lancé lors du new du second élément (c'est à dire comment faire pour que le premier élément soit correctement libéré).

+1 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte