Storia della questione:
C++ ha ereditato il concetto di ereditarietà dalle classi del linguaggio C++ e dalle metodologie orientate agli oggetti. L'emergere dell'ereditarietà multipla e virtuale ha complicato la struttura delle gerarchie, aumentando la flessibilità ma introducendo nuovi tipi di errori.
Problema:
L'ereditarietà diretta è una situazione in cui una classe eredita direttamente da un'altra senza complessità aggiuntive. L'ereditarietà indiretta si verifica quando i membri ereditati provengono attraverso una o più catene di ereditarietà intermedie. La difficoltà principale è il "problema del diamante" (diamond problem), in cui più percorsi verso una singola classe base possono portare a una duplicazione dei suoi membri nelle classi derivate.
Soluzione:
Per gestire la complessità viene utilizzata l'ereditarietà virtuale, che fornisce un'unica istanza comune di un membro della classe base per l'intera gerarchia, anziché per ogni catena.
Esempio di codice:
class A { public: int value; }; class B : public virtual A {}; class C : public virtual A {}; class D : public B, public C {};
Nella classe D ci sarà solo una istanza del membro A::value.
Caratteristiche chiave:
L'ereditarietà virtuale può causare comportamenti indefiniti se non viene utilizzata in presenza di un problema a diamante?
Se nell'gerarchia a diamante non viene utilizzata l'ereditarietà virtuale, i membri della classe base saranno duplicati. Ciò può confondere la logica del codice e portare a ambiguità nell'accesso ai membri della classe base.
In quali casi non è necessario utilizzare l'ereditarietà virtuale?
Se si è certi che le proprie gerarchie non formino una struttura a diamante, o se la classe base non contiene dati, l'ereditarietà virtuale non è necessaria.
Come gestire la chiamata ai costruttori durante l'ereditarietà virtuale?
Il costruttore della classe base virtuale viene chiamato solo dalla classe derivata "più in basso". Nelle classi intermedie non è consentito specificare argomenti per la classe base virtuale (il costruttore viene chiamato per default se non inizializzato esplicitamente nella classe finale).
Esempio di codice:
class A { public: A(int x) { /* ... */ } }; class B : public virtual A { public: B() : A(0) {} // errore: non è possibile inizializzare A qui }; class D : public B { public: D() : A(10), B() {} // corretto };
In un grande progetto, uno sviluppatore non ha notato l'emergere della struttura a diamante: entrambe le classi intermedie erano ereditate direttamente da una base. L'accesso ai membri della classe base ha causato ambiguità e il codice era difficile da leggere.
Pro:
Contro:
L'architetto ha notato il problema in anticipo e ha utilizzato l'ereditarietà virtuale per le classi intermedie, e il costruttore della classe base virtuale è stato chiamato correttamente solo nella classe derivata più in basso.
Pro:
Contro: