Perché un'altra risposta?
Bene, molti post su SO e articoli esterni dicono che il problema del diamante viene risolto creando una singola istanza Ainvece di due (una per ogni genitore di D), risolvendo così l'ambiguità. Tuttavia, questo non mi ha dato una comprensione completa del processo, mi sono ritrovato con ancora più domande come
- cosa succede se
Be Ccerca di creare diverse istanze, Aad esempio chiamando il costruttore parametrizzato con parametri diversi ( D::D(int x, int y): C(x), B(y) {})? Di quale istanza Averrà scelto di entrare a far parte D?
- cosa succede se uso l'ereditarietà non virtuale per
B, ma virtuale per C? È sufficiente per creare una singola istanza di Ain D?
- dovrei sempre utilizzare l'ereditarietà virtuale di default d'ora in poi come misura preventiva poiché risolve un possibile problema del diamante con un costo di prestazione minore e senza altri inconvenienti?
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 capire l'eredità virtuale.
Doppia A
Innanzitutto, iniziamo con questo codice senza ereditarietà virtuale:
#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;
}
Esaminiamo l'output. L'esecuzione 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 di entrambi Be C, ognuno di loro crea il proprio A, quindi abbiamo il doppio Ain 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 d.getX()causare un errore di compilazione in quanto il compilatore non può scegliere per quale Aistanza deve chiamare il metodo. È comunque possibile chiamare i metodi direttamente per la classe genitore scelta:
d.B::getX() = 3
d.C::getX() = 2
Virtualità
Ora aggiungiamo l'eredità virtuale. Utilizzando lo stesso esempio di codice con le seguenti modifiche:
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
...
Passiamo alla creazione di d:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
Come puoi vedere, Aviene creato con il costruttore predefinito che ignora i parametri passati dai costruttori di Be C. Poiché l'ambiguità è scomparsa, tutte le chiamate per getX()restituire lo stesso valore:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Ma cosa succede se vogliamo chiamare un costruttore parametrizzato 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 classe può utilizzare esplicitamente solo i costruttori di genitori diretti, ma esiste un'esclusione per il caso di ereditarietà virtuale. La scoperta di questa regola ha "cliccato" per me e mi ha aiutato a capire molto le interfacce virtuali:
Codice class B: virtual Asignifica che qualsiasi classe ereditata da Bè ora responsabile della creazione Ada sola, poiché Bnon lo farà automaticamente.
Con questa affermazione in mente è facile rispondere a tutte le domande che avevo:
- Durante la
Dcreazione né Bné Cè responsabile per i parametri di A, dipende totalmente da Dsolo.
Cdelegherà la creazione di Aa D, ma Bcreerà una propria istanza Aper riportare in tal modo il problema dei diamanti
- Definire i parametri della classe base nella classe nipote piuttosto che nel figlio diretto non è una buona pratica, quindi dovrebbe essere tollerata quando esiste il problema del diamante e questa misura è inevitabile.