In che modo l’ereditarietà virtuale risolve l’ambiguità “diamante” (eredità multipla)?

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<eat(); } 

Capisco il problema dei diamanti, e al di sopra del codice non ho questo problema.

In che modo esattamente l’ereditarietà virtuale risolve il problema?

Quello che capisco: quando dico A *a = new D(); , il compilatore vuole sapere se un object di tipo D può essere assegnato a un puntatore di tipo A , ma ha due percorsi che può seguire, ma non può decidere da solo.

Quindi, in che modo l’ereditarietà virtuale risolve il problema (aiutare il compilatore a prendere la decisione)?

Vuoi: (Realizzabile con eredità virtuale)

  A / \ BC \ / D 

E non: (Cosa succede senza eredità virtuale)

 AA | | BC \ / D 

L’ereditarietà virtuale significa che ci sarà solo 1 istanza della class base A non 2.

Il tuo tipo D avrebbe 2 puntatori vtable (puoi vederli nel primo diagramma), uno per B e uno per C che virtualmente eredita A La dimensione dell’object di D è aumentata perché ora memorizza 2 puntatori; tuttavia c’è solo una A ora.

Quindi B::A e C::A sono uguali e quindi non ci possono essere chiamate ambigue da D Se non usi l’ereditarietà virtuale, hai il secondo diagramma sopra. E ogni chiamata a un membro di A diventa ambigua e devi specificare quale percorso vuoi intraprendere.

Wikipedia ha un’altra buona carrellata ed esempio qui

Le istanze di classi derivate “contengono” istanze di classi base, quindi appaiono in memoria in questo modo:

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

Pertanto, senza l’ereditarietà virtuale, l’istanza della class D sarà simile a:

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

Quindi, prendi nota di due “copie” di dati A. L’ereditarietà virtuale significa che all’interno della class derivata esiste un puntatore vtable impostato in runtime che punta ai dati della class base, in modo che le istanze delle classi B, C e D assomiglino:

 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 

Il problema non è il percorso che il compilatore deve seguire. Il problema è l’ endpoint di quel percorso: il risultato del cast. Quando si tratta di digitare conversioni, il percorso non ha importanza, solo il risultato finale.

Se usi l’ereditarietà ordinaria, ogni percorso ha il proprio endpoint distintivo, il che significa che il risultato del cast è ambiguo, che è il problema.

Se si utilizza l’ereditarietà virtuale, si ottiene una gerarchia a forma di diamante: entrambi i percorsi conducono allo stesso endpoint. In questo caso il problema di scegliere il percorso non esiste più (o, più precisamente, non conta più), perché entrambi i percorsi portano allo stesso risultato. Il risultato non è più ambiguo: questo è ciò che conta. Il percorso esatto no.

In realtà l’esempio dovrebbe essere il seguente:

 #include  //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; } 

… in questo modo l’output sarà quello corretto: “EAT => D”

L’ereditarietà virtuale risolve solo la duplicazione del nonno! MA hai ancora bisogno di specificare i metodi per essere virtuale al fine di ottenere i metodi correttamente sovrascritti …

Perché un’altra risposta?

Bene, molti post su SO e articoli esterni dicono che il problema dei diamanti viene risolto creando una singola istanza di A anziché due (una per ciascun genitore di D ), risolvendo così l’ambiguità. Tuttavia, questo non mi ha dato una comprensione completa del processo, ho finito con ancora più domande come

  1. cosa succede se B e C tenta di creare diverse istanze di A ad esempio chiamando il costruttore parametrizzato con parametri diversi ( D::D(int x, int y): C(x), B(y) {} )? Quale istanza di A sarà scelta per diventare parte di D ?
  2. cosa succede se utilizzo l’ereditarietà non virtuale per B , ma quella virtuale per C ? È sufficiente creare una singola istanza di A in D ?
  3. dovrei sempre utilizzare l’ereditarietà virtuale di default da ora in poi come misura preventiva poiché risolve il ansible problema dei diamanti con minori costi di prestazione e nessun altro inconveniente?

Non essere in grado di prevedere il comportamento senza provare esempi di codice significa non comprendere il concetto. Di seguito è riportato ciò che mi ha aiutato a comprendere l’ereditarietà virtuale.

Double A

Innanzitutto, iniziamo con questo codice senza ereditarietà virtuale:

 #include 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 << "dA::getX() = " << dA::getX() << endl; cout << "dB::getX() = " << dB::getX() << endl; cout << "dC::getX() = " << dC::getX() << endl; } 

Passiamo attraverso l'output. Eseguendo B b(2); crea A(2) come previsto, lo stesso per 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); ha bisogno sia di B che di C , ognuno dei quali crea il proprio A , quindi abbiamo il doppio A in d :

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

Questo è il motivo per cui d.getX() causa errori di compilazione in quanto il compilatore non può scegliere per quale istanza deve chiamare il metodo. È ancora ansible chiamare i metodi direttamente per la class genitore scelta:

 dB::getX() = 3 dC::getX() = 2 

Virtuality

Ora consente di aggiungere l'ereditarietà virtuale. Utilizzando lo stesso codice di esempio con le seguenti modifiche:

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

Consente di saltare alla creazione di d :

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

È ansible vedere che A viene creato con il costruttore predefinito che ignora i parametri passati dai costruttori di B e C Man mano che l'ambiguità scompare, tutte le chiamate a getX() restituiscono lo stesso valore:

 d.getX() = 42 dA::getX() = 42 dB::getX() = 42 dC::getX() = 42 

Ma cosa succede se vogliamo chiamare il costruttore parametrico per A ? Può essere fatto chiamandolo esplicitamente dal costruttore di D :

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

Normalmente, la class può utilizzare esplicitamente solo costruttori di genitori diretti, ma esiste un'esclusione per il caso dell'ereditarietà virtuale. La scoperta di questa regola mi ha "fatto clic" e mi ha aiutato a capire molto le interfacce virtuali:

Codice di class B: virtual A significa che qualsiasi class ereditata da B è ora responsabile della creazione di A da sola, poiché B non lo farà automaticamente.

Con questa affermazione in mente è facile rispondere a tutte le domande che ho avuto:

  1. Durante la creazione di DBC è responsabile dei parametri di A , è totalmente fino a D solo.
  2. C delegherà la creazione di A a D , ma B creerà la propria istanza di A riportando così il problema dei diamanti
  3. Definire i parametri della class base in class grandchild piuttosto che come figlio diretto non è una buona pratica, quindi dovrebbe essere tollerato quando il problema dei diamanti esiste e questa misura è inevitabile.

L’esempio di codice corretto è qui. Il problema dei diamanti:

 #include  // 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 soluzione :

 #include  // 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; } 

Questo problema può essere risolto utilizzando la parola chiave virtuale.

  A / \ BC \ / D 

Esempio di problema del diamante.

 #include using namespace std; class AA { public: int a; AA() { a=10; } }; class BB: virtual public AA { public: int b; BB() { b=20; } }; class CC:virtual public AA { public: int c; CC() { c=30; } }; class DD:public BB,CC { public: int d; DD() { d=40; printf("Value of A=%d\n",a); } }; int main() { DD dobj; return 0; }