Il problema dell'ambiguita` che si verifica con
l'ereditarieta` multipla, puo` essere portato al caso estremo in cui una classe
ottenuta per ereditarieta` multipla erediti piu` volte una stessa classe base:
class BaseClass {
/* ... */
};
class Derived1 : public BaseClass {
/* ... */
};
class Derived2 : private BaseClass {
/* ... */
};
class Derived3 : public Derived1, private Derived2 {
/* ... */
};
Di nuovo quello che succede e` che alcuni membri (in particolare tutta una classe)
sono duplicati nella classe Derived3 (anche se una copia di questi non
e` immediatamente accessibile dalla classe derivata).
Consideriamo l'immagine in memoria di una istanza della classe Derived3,
la situazione che avremmo sarebbe la seguente:
La classe Derived3
contiene una istanza di ciasciuna delle sue classi base dirette:
Derived1 e Derived2.
Ognuna di esse contiene a sua volta una istanza della classe base
BaseClass e opera esclusivamente su tale istanza.
In alcuni casi situazioni di questo tipo non creano problemi, ma in generale si
tratta di una possibile fonte di inconsistenza.
Supponiamo ad esempio di avere una classe Person e di derivare da essa
prima una classe Student e poi una classe Employee al
fine di modellare un mondo di persone che eventualmente possono essere studenti o
impiegati; dopo un po' ci accorgiamo che una persona puo` essere contemporaneamente
uno studente ed un lavoratore, cosi` tramite l'ereditarieta` multipla deriviamo da
Student e Employee la classe Student-Employee.
Il problema e` che la nuova classe contiene due istanze della classe Person
e queste due istanze vengono accedute (in lettura e scrittura) indipendentemente
l'una dall'altra... Cosa accadrebbe se nelle due istanze venissero memorizzati dati
diversi? Una gravissima forma di inconsistenza!
La soluzione viene chiamata ereditarieta` virtuale, e la si utilizza nel seguente
modo:
class Person {
/* ... */
};
class Student : virtual private Person {
/* ... */
};
class Employee : virtual private Person {
/* ... */
};
class Student-Employee : private Student, private Employee {
/* ... */
};
Quando una classe eredita tramite la keyword virtual il compilatore non si
limita a copiare il contenuto della classe base nella classe derivata, ma inserisce
nella classe derivata un puntatore ad una istanza della classe base. Quando una
classe eredita (per ereditarieta` multipla) piu` volte una classe base virtuale
(e` questo il caso di Student-Employee che eredita piu` volte
Person), il compilatore inserisce solo una istanza della classe
virtuale e fa si che tutti i puntatori a tale classe puntino a quell'unica istanza.
La situazione in questo caso e` illustata dalla seguente figura:
La classe Student-Employee
contiene ancora una istanza di ciasciuna delle sue classi base dirette:
Student e Employee, ma ora esiste una sola istanza
della classe base indiretta Person poiche` essa e` stata dichiarata
virtual nelle definizioni di Student e
Employee.
Il puntatore alla classe base virtuale non e` visibile al programmatore, non bisogna
tener conto di esso poiche` viene aggiunto dal compilatore a compile-time,
semplicemente si accede ai membri della classe base virtuale come si farebbe con una
normale classe base.
Il vantaggio di questa tecnica e` che non e` piu` necessario definire la classe
Student-Employee derivandola da Student (al fine di
eliminare la fonte di inconsistenza) e aggiungendo a mano le definizioni di
Employee, in tal modo si risparmiano tempo e fatica riducendo la
quantita` di codice da produrre e limitando la possibilita` di errori e la quantita`
di memoria necessaria al nostro programma per girare.
C'e` pero` un costo da pagare: un livello di indirezione in piu` perche` l'accesso
alle classi base virtuali (nell'esempio Person) avviene tramite
un puntatore.
L'ereditarieta` virtuale genera anche un nuovo problema: il costruttore di una
classe derivata chiama i costruttori delle classi base e nel caso di ereditarieta`
virtuale una determinata classe base virtuale potrebbe essere inizializzata piu`
volte.
Nel nostro esempio la classe base virtuale Person e` inizializzata
sia da Student che da Employee, entrambe le classi hanno
il dovere di eseguire la chiamata al costruttore della classe base, ma quando queste
due classi vengono fuse per derivare la classe Student-Employee il
costruttore della nuova classe, chiamando i costruttori di Student
e Employee, implicitamente chiamerebbe due volte il costruttore di
Person.
Per evitare tale comportamento e` stato deciso che i costruttori di una classe che
possieda una classe base (diretta o indiretta) virtuale debbano eseguire
esplicitamente una chiamata ad un costruttore della classe virtuale, il compilatore
fara` poi in modo che l'inizializzazione sia effettivamente eseguita solo dalla
classe massimamente derivata (ovvero quella cui appartiene l'istanza che si sta
creando).
In questo modo ogni classe base virtuale e` inizializzata una sola volta e in modo
deterministico.
Il seguente codice
opportunamente completato, produrrebbe il seguente output:
Definizione di Tizio:
Costruttore Person invocato...
Definizione di Caio:
Costruttore Person invocato...
Costruttore Student invocato...
Definizione di Sempronio:
Costruttore Person invocato...
Costruttore Employee invocato...
Definizione di Sempronio:
Costruttore Person invocato...
Costruttore Student invocato...
Costruttore Employee invocato...
Costruttore Student-Employee invocato...
Come potete osservare il costruttore della classe Person viene
invocato una sola volta, per verificare poi da chi viene invocato basta tracciare
l'esecuzione con un debugger simbolico.
Analogamente anche i distruttori seguono una regola simile, solo che in questo caso
tutto il lavoro e` svolto dal compilatore (i distruttori non devono mai essere
invocati esplicitamente).