Classi base virtuali
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, public Derived2 {
/* ... */
};
Di nuovo quello che succede e` che alcuni membri (in particolare tutta una classe)
sono duplicati nella classe Derived3 .
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 public Person {
/* ... */
};
class Employee : virtual public Person {
/* ... */
};
class Student-Employee : public Student,
public Employee {
/* ... */
};
Si tratta di un esempio che nella pratica non avrebbe alcuna validita`, ma
ottimo da un punto di vista didattico.
Vediamo piu` in dettaglio cosa e cambiato e come virtual opera.
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 da 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` illustrata 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,
tutto il meccanismo e` completamente trasparente, 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.
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 risolve dunque l'ambiguita` di cui sopra, nelle classi
derivate le definizioni di una classe base virtuale sono presenti una volta
sola, tuttavia il problema dell'ambiguita` non e` del tutto risolto, esistono
ancora situazioni in cui il problema si ripropone. Supponiamo ad esempio che
una delle classi intermedie ridefinisca una funzione membro della classe base:
class Person {
public:
void DoSomething();
/* ... */
};
class Student : virtual public Person {
public:
void DoSomething();
/* ... */
};
class Employee : virtual public Person {
public:
void DoSomething();
/* ... */
};
class Student-Employee : public Student,
public Employee {
/* ... */
};
Se Student-Employee non ridefinisce il metodo DoSomething() ,
la situazione seguente presenterebbe ancora ambiguita`:
Student-Employee Caio;
/* ... */
Caio.DoSomething(); // Ambiguo!
perche` la classe Student-Employee eredita nuovamente due diverse
definizioni del metodo DoSomething() .
Esiste anche un caso apparentemente ambiguo e simile al precedente:
class Person {
public:
void DoSomething();
/* ... */
};
class Student : virtual public Person {
public:
void DoSomething();
/* ... */
};
class Employee : virtual public Person {
/* ... */
};
class Student-Employee : public Student,
public Employee {
/* ... */
};
La situazione e` pero` assai diversa, in questo caso solo una delle due classi
base dirette ridefinisce il metodo ereditato da Person ; in Student-Employee
abbiamo ancora due definizioni di DoSomething() , ma una e` in un
certo senso "piu` aggiornata" delle altre. In situazioni del genere si dice
che Student::DoSomething() domina Person::DoSomething()
e in questi casi ambiguita` tipo
Student-Employee Caio;
/* ... */
Caio.DoSomething();
vengono risolte dal compilatore in favore della definizione dominante.
Si noti che ci deve essere una sola definizione che domina tutte le altre,
altrimenti ci sarebbe ancora ambiguita`.
Ritorniamo a parlare a proposito della classe base virtuale.
Nei vari costruttori delle classi derivate c'e` implicitamente o esplicitamente
una chiamata al costruttore della classe base virtuale, ma ora abbiamo una sola
istanza di tale classe e non possiamo certo inizializzarla 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 .
E` necessario stabilire un criterio deterministico che stabilisca chi deve inizializzare
la classe virtuale. Lo standard stabilisce che il compito di inizializzare la
classe base virtuale spetta alla classe massimamente derivata. La classe
massimamente derivata e` quella che noi stiamo istanziando: se vogliamo creare
un oggetto di tipo Student la classe massimamente derivata e` in
questo caso Student , se invece stiamo istanziando Student-Employee
allora e` quest'ultima la classe massimamente derivata.
E` dunque il costruttore della classe massimamente derivata che inizializza
la classe virtuale.
Il seguente codice
Person::Person() {
cout << "Costruttore Person invocato..." << endl;
}
Student::Student() : Person() {
cout << "Costruttore Student invocato..." << endl;
}
Employee::Employee() : Person() {
cout << "Costruttore Employee invocato..." << endl;
}
Student-Employee::Student-Employee()
: Person(), Student(), Employee() {
cout << "Costruttore Student-Employee invocato..."
<< endl;
}
/* ... */
cout << "Definizione di Tizio:" << endl;
Person Tizio;
cout << endl << "Definizione di Caio:" << endl;
Student Caio;
cout << endl << "Definizione di Sempronio:" << endl;
Employee Sempronio;
cout << endl << "Definizione di Bruto:" << endl;
Student-Employee Bruto;
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 Bruto:
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.
Naturalmente ci sarebbe un problema simile anche con il distruttore, bisogna
evitare che si tenti di distruggere la classe base virtuale piu` volte; nuovamente
e` il compilatore che si assume l'onere di fare in modo che l'operazione venga
eseguita una sola volta.
|