Perché un'altra risposta?
Bene, molti post su SO e articoli esterni dicono che il problema del diamante viene risolto creando una singola istanza A
invece 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
B
e C
cerca di creare diverse istanze, A
ad esempio chiamando il costruttore parametrizzato con parametri diversi ( D::D(int x, int y): C(x), B(y) {}
)? Di quale istanza A
verrà 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 A
in 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 B
e C
, ognuno di loro 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 d.getX()
causare un errore di compilazione in quanto il compilatore non può scegliere per quale A
istanza 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, A
viene creato con il costruttore predefinito che ignora i parametri passati dai costruttori di B
e 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 A
significa che qualsiasi classe ereditata da B
è ora responsabile della creazione A
da sola, poiché B
non lo farà automaticamente.
Con questa affermazione in mente è facile rispondere a tutte le domande che avevo:
- Durante la
D
creazione né B
né C
è responsabile per i parametri di A
, dipende totalmente da D
solo.
C
delegherà la creazione di A
a D
, ma B
creerà una propria istanza A
per 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.