web-dev-qa-db-fra.com

Comment l'héritage virtuel résout-il l'ambiguïté du "diamant" (héritage multiple)?

class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

Je comprends le problème des diamants, et le code ci-dessus n’a pas ce problème.

Comment exactement l'héritage virtuel résout-il le problème?

Ce que je comprends: Quand je dis A *a = new D();, le compilateur veut savoir si un objet de type D peut être affecté à un pointeur de type A, mais il a deux chemins qu'il peut suivre, mais ne peut pas décider par lui-même.

Alors, comment l'héritage virtuel résout-il le problème (aide le compilateur à prendre la décision)?

70
Moeb

Vous voulez: (réalisable avec un héritage virtuel)

  A  
 / \  
B   C  
 \ /  
  D 

Et pas: (Que se passe-t-il sans héritage virtuel)

A   A  
|   |
B   C  
 \ /  
  D 

L'héritage virtuel signifie qu'il n'y aura qu'une seule instance de la classe de base A et non 2. 

Votre type D aurait 2 pointeurs vtable (vous pouvez les voir dans le premier diagramme), un pour B et un pour C qui héritent virtuellement de A. La taille de l'objet D est augmentée car il stocke 2 pointeurs maintenant; Cependant, il n’ya plus qu’un seul A pour le moment. 

Donc, B::A et C::A sont identiques et il ne peut y avoir d'appels ambigus de D. Si vous n'utilisez pas l'héritage virtuel, vous avez le deuxième diagramme ci-dessus. Et tout appel à un membre de A devient alors ambigu et vous devez spécifier le chemin que vous souhaitez emprunter.

Wikipedia a un autre bon aperçu et exemple ici

82
Brian R. Bondy

Les instances de classes dérivées "contiennent" des instances de classes de base, elles ont donc l'apparence suivante en mémoire:

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]

Ainsi, sans héritage virtuel, une instance de classe D ressemblerait à ceci:

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'

Donc, notez deux "copies" de A data. L'héritage virtuel signifie qu'à l'intérieur de la classe dérivée, un pointeur vtable défini au moment de l'exécution pointe vers les données de la classe de base, de sorte que les instances des classes B, C et D se présentent comme suit:

class B: [A fields | B fields]
          ^---------- pointer to A

class C: [A fields | C fields]
          ^---------- pointer to A

class D: [A fields | B fields | C fields | D fields]
          ^---------- pointer to B::A
          ^--------------------- pointer to C::A
39
el.pescado

Pourquoi une autre réponse?

Eh bien, de nombreux articles sur SO et des articles à l'extérieur disent que ce problème de diamant est résolu en créant une seule instance de A au lieu de deux (une pour chaque parent de D), ce qui résout l'ambiguïté. Cependant, cela ne me donnait pas une compréhension complète du processus, je me suis retrouvé avec encore plus de questions comme

  1. et si B et C essaye de créer différentes instances de A par ex. appelant constructeur paramétrée avec différents paramètres (D::D(int x, int y): C(x), B(y) {})? Quelle instance de A sera choisie pour faire partie de D?
  2. et si j'utilise l'héritage non virtuel pour B, mais virtuel pour C? Est-ce suffisant pour créer une seule instance de A dans D?
  3. devrais-je toujours utiliser désormais l'héritage virtuel par défaut à titre préventif, car il résout un éventuel problème lié aux diamants avec un coût de performances minime et aucun autre inconvénient?

Ne pas être capable de prédire le comportement sans avoir essayé des exemples de code signifie ne pas comprendre le concept. Vous trouverez ci-dessous ce qui m’a aidé à comprendre l’héritage virtuel. 

Double A

Commençons par ce code sans héritage virtuel:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

Permet de parcourir la sortie. L'exécution de B b(2); crée A(2) comme prévu, de même pour C c(3);:

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3); a besoin de B et C, chacun d’eux créant sa propre A; nous avons donc le double A dans d:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

C'est la raison pour laquelle d.getX() cause une erreur de compilation car le compilateur ne peut pas choisir l'occurrence A pour laquelle il doit appeler une méthode. Il est toujours possible d'appeler des méthodes directement pour la classe parent choisie:

d.B::getX() = 3
d.C::getX() = 2

Virtualité

Ajoutons maintenant l'héritage virtuel. Utiliser le même exemple de code avec les modifications suivantes:

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

Permet de passer à la création de d:

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

Vous pouvez voir que A est créé avec le constructeur par défaut en ignorant les paramètres transmis par les constructeurs de B et C. L'ambiguïté ayant disparu, tous les appels à getX() renvoient la même valeur:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

Mais que faire si on veut appeler un constructeur paramétré pour A? Cela peut être fait en l'appelant explicitement à partir du constructeur de D:

D(int x, int y, int z): A(x), C(y), B(z)

Normalement, la classe ne peut utiliser explicitement que les constructeurs de parents directs, mais il existe une exclusion pour le cas d'héritage virtuel. La découverte de cette règle a "cliqué" pour moi et m'a beaucoup aidé à comprendre les interfaces virtuelles:

Code class B: virtual A signifie que toute classe héritée de B est désormais responsable de la création de A par elle-même, puisque B ne le fera pas automatiquement.

Avec cette déclaration en tête, il est facile de répondre à toutes les questions que j'avais:

  1. Lors de la création de D, ni B ni C n'est responsable des paramètres de A, elle est totalement jusqu'à D uniquement.
  2. C déléguera la création de A à D, mais B créera sa propre instance de A, ramenant ainsi le problème de diamant
  3. Définir les paramètres de la classe de base dans la classe des petits-enfants plutôt que dans les enfants directs n'est pas une bonne pratique. Cette méthode doit donc être tolérée en cas de problème de diamant et cette mesure est inévitable.
10
nnovich-OK

Le problème n'est pas le cheminque le compilateur doit suivre. Le problème est le endpoint de ce chemin: le résultat de la conversion. Quand il s’agit de taper des conversions, le chemin n’importe pas, seul le résultat final l’importe.

Si vous utilisez un héritage ordinaire, chaque chemin a son propre point de terminaison distinct, ce qui signifie que le résultat de la conversion est ambigu, ce qui constitue le problème.

Si vous utilisez l'héritage virtuel, vous obtenez une hiérarchie en forme de losange: les deux chemins mènent au même point de terminaison. Dans ce cas, le problème du choix du chemin n'existe plus (ou, plus précisément, ne compte plus), car les deux chemins mènent au même résultat. Le résultat n'est plus ambigu - c'est ce qui compte. Le chemin exact ne le fait pas.

8
AnT

En réalité, l'exemple devrait être le suivant:

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

... ainsi, la sortie sera la bonne: "EAT => D"

L'héritage virtuel ne résout que la duplication du grand-père!.

7
enger

L'exemple de code correct est ici. Le problème du diamant:

#include <iostream>
// Here you have the diamond problem : there is B::eat() and C::eat()
// because they both inherit from A and contain independent copies of A::eat()
// So what is D::eat()? Is it B::eat() or C::eat() ?
class A { public: void eat(){ std::cout << "CHROME-CHROME" << endl; } };
class B: public A   { };
class C: public A   { };
class D: public B,C { };

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

La solution :

#include <iostream>
// Virtual inheritance to ensure B::eat() and C::eat() to be the same 
class A { public: void eat(){ std::cout<< "CHROME-CHROME" << endl; } };
class B: virtual public A   { };
class C: virtual public A   { };
class D: public         B,C { };

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}
0
tkrishtop