119 votes

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 du diamant, et le code ci-dessus n'a pas ce problème.

Comment l'héritage virtuel résout-il exactement 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 assigné à un pointeur de type A Il a deux voies qu'il peut suivre, mais il ne peut pas décider tout seul.

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

140voto

Brian R. Bondy Points 141769

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

  A  
 / \  
B   C  
 \ /  
  D 

Et non : (Que se passe-t-il en l'absence d'héritage virtuel ?)

A   A  
|   |
B   C  
 \ /  
  D 

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

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

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

Wikipedia propose un autre bon résumé et un exemple ici

101voto

nnovich-OK Points 1554

Pourquoi une autre réponse ?

De nombreux posts sur SO et articles à l'extérieur disent que le problème du diamant est résolu par la création d'une instance unique de A au lieu de deux (un pour chaque parent de D ), ce qui permet de lever l'ambiguïté. Cependant, cela ne m'a pas permis d'acquérir une compréhension complète du processus, et j'ai fini par me poser encore plus de questions, comme par exemple

  1. et si B y C tente de créer différentes instances de A par exemple en appelant un constructeur paramétré avec différents paramètres ( D::D(int x, int y): C(x), B(y) {} ) ? Quelle instance de A seront choisis pour faire partie de la D ?
  2. Que se passe-t-il si j'utilise l'héritage non virtuel pour B mais virtuel pour les C ? Est-ce suffisant pour créer une seule instance de A en D ?
  3. Devrais-je toujours utiliser l'héritage virtuel par défaut à partir de maintenant comme mesure préventive puisqu'il résout le problème du diamant possible avec un coût de performance mineur et aucun autre inconvénient ?

Ne pas être capable de prédire le comportement sans essayer des échantillons de code signifie ne pas comprendre le concept. Voici 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;
}

Passons en revue les résultats. Exécution B b(2); crée A(2) comme prévu, idem 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 à la fois B y C chacun d'entre eux créant son propre A Nous avons donc un double A en 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() provoquerait une erreur de compilation, car le compilateur ne peut pas choisir le type de A instance pour laquelle il doit appeler la méthode. Il est toutefois possible d'appeler directement les méthodes de la classe mère choisie :

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

Virtualité

Ajoutons maintenant l'héritage virtuel. En utilisant 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
...

Passons à la création de d :

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

Vous pouvez voir, A est créé avec un constructeur par défaut qui ignore les paramètres transmis par les constructeurs de B y C . L'ambiguïté étant levée, 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 qu'en est-il si nous voulons appeler le constructeur paramétré de A ? Il est possible de le faire en l'appelant explicitement à partir du constructeur de D :

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

Normalement, une classe ne peut utiliser explicitement que les constructeurs de ses parents directs, mais il existe une exclusion pour les cas d'héritage virtuel. La découverte de cette règle m'a fait "tilt" 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 lui-même, puisque B ne le fera pas automatiquement.

Avec cette déclaration à l'esprit, il est facile de répondre à toutes les questions que je me posais :

  1. Pendant D la création non plus B ni C est responsable des paramètres de A , c'est à vous de décider D seulement.
  2. C déléguera la création de A a D mais B créera sa propre instance de A ramenant ainsi le problème du diamant
  3. Définir les paramètres de la classe de base dans la classe du petit-enfant plutôt que dans celle de l'enfant direct n'est pas une bonne pratique, elle doit donc être tolérée lorsqu'il existe un problème de diamant et que cette mesure est inévitable.

50voto

el.pescado Points 7960

Les instances des classes dérivées stockent les membres de leurs classes de base.

Sans héritage virtuel, les schémas de mémoire se présentent comme suit (notez les deux des copies de la A dans la classe D ):

class A: [A members]
class B: public A [A members|B members]
class C: public A [A members|C members]
class D: public B, public C [A members|B members|A members|C members|D members]

Avec l'héritage virtuel, les schémas de mémoire se présentent comme suit (notez les unique copie de la A dans la classe D ):

class A: [A members]
class B: virtual public A [B members|A members]
                           |         ^
                           v         |
                         virtual table B

class C: virtual public A [C members|A members]
                           |         ^
                           v         |
                         virtual table C

class D: public B, public C [B members|C members|D members|A members]
                             |         |                   ^
                             v         v                   |
                           virtual table D ----------------|

Pour chaque classe dérivée, le compilateur crée une table virtuelle contenant des pointeurs vers les membres de ses classes de base virtuelles stockés dans la classe dérivée, et ajoute un pointeur vers cette table virtuelle dans la classe dérivée.

13voto

AndreyT Points 139512

Le problème n'est pas le chemin le compilateur doit suivre. Le problème est que le point final de ce chemin : le résultat du lancer. Lorsqu'il s'agit de conversions de types, le chemin n'a pas d'importance, seul le résultat final en a.

Si vous utilisez l'héritage ordinaire, chaque chemin a son propre point d'arrivée, ce qui signifie que le résultat de la conversion est ambigu, et c'est là que le bât blesse.

Si vous utilisez l'héritage virtuel, vous obtenez une hiérarchie en forme de diamant : les deux chemins mènent au même point final. Dans ce cas, le problème du choix du chemin n'existe plus (ou, plus précisément, n'a plus d'importance), 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 n'a pas d'importance.

11voto

enger Points 1

En fait, 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;
}

... de cette façon, la sortie sera correcte : "EAT=>D"

L'héritage virtuel ne résout que le problème de la duplication du grand-père ! MAIS il faut toujours spécifier que les méthodes sont virtuelles pour qu'elles soient correctement remplacées...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X