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
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|