Funzioni virtuali
Il meccanismo dell'ereditarieta` e` stato gia` di per se una grande innovazione
nel mondo della programmazione, tuttavia le sorprese non si esauriscono qui.
Esiste un'altra caratteristica tipica dei linquaggi a oggetti (C++ incluso)
che ha valso loro il soprannome di "Linguaggi degli attori": la possibilita`
di avere oggetti capaci di "recitare" di volta in volta il ruolo piu` appropriato,
ma andiamo con ordine.
L'ereditarieta` pone nuove regole circa la compatibilita` dei tipi, in particolare
se Ptr e` un puntatore di tipo T , allora Ptr
puo` puntare non solo a istanze di tipo T ma anche a istanze di
classi derivate da T (sia tramite ereditarieta` semplice che multipla).
Se Td e` una classe derivata (anche indirettamente) da T ,
istruzioni del tipo
T* Ptr = 0; // Puntatore nullo
/* ... */
Ptr = new Td;
sono assolutamente lecite e il compilatore non segnala errori o warning.
Cio` consente ad esempio la realizzazione di una lista per contenere tutta una
serie di istanze di una gerarchia di classi, magari per poter eseguire un loop
su di essa e inviare a tutti gli oggetti della lista uno stesso messaggio. Pensate
ad esempio ad un programma di disegno che memorizza gli oggetti disegnati mantenendoli
in una lista, ogni oggetto sa come disegnarsi e se e` necessario ridisegnare
tutto il disegno basta scorrere la lista inviando ad ogni oggetto il messaggio
di Paint.
Purtroppo la cosa cosi` com'e` non puo` funzionare poiche` le funzioni sono
linkate staticamente dal linker. Anche se tutte le classi della gerarchia possiedono
un metodo Paint() , noi sappiamo solo che Ptr punta
ad un oggetto di tipo T o T -derivato,
non conoscendo l'esatto tipo una chiamata a Ptr->Paint() non
puo` che essere risolta chiamando Ptr->T::Paint() (che non fara`
in generale cio` che vorremmo). Il compilatore non puo` infatti rischiare di
chiamare il metodo di una classe derivata, poiche` questo potrebbe tentare di
accedere a membri che non fanno parte dell'effettivo tipo dell'oggetto (causando
inconsistenze o un crash del sistema), chiamando il metodo della classe T
al piu` il programma non fara` la cosa giusta, ma non mettera` in pericolo la
sicurezza e l'affidabilita` del sistema (perche` un oggetto derivato possiede
tutti i membri della classe base).
Si potrebbe risolvere il problema inserendo in ogni classe della gerarchia un
campo che stia ad indicare l'effettivo tipo dell'istanza:
enum TypeId { T-Type, Td-Type };
class T {
public:
TypeId Type;
/* ... */
private:
/* ... */
};
class Td : public T {
/* ... */
};
e risolvere il problema con una istruzione switch :
switch (Ptr->Type) {
case T-Type : Ptr->T::Paint();
break;
case Td-Type : Ptr->Td::Paint();
break;
default : /* errore */
};
Una soluzione di questo tipo funziona ma e` macchinosa, allunga il lavoro e
una dimenticanza puo` costare cara, e soprattutto ogni volta che si modifica
la gerarchia di classi bisogna modificare anche il codice che la usa.
La soluzione migliore e` invece quella di far in modo che il corretto tipo dell'oggetto
puntato sia automaticamente determinato al momento della chiamata della funzione
e rinviando il linking di tale funzione a run-time.
Per fare cio` bisogna dichiarare la funzione membro virtual :
class T {
public:
/* ... */
virtual void Paint();
private:
/* ... */
};
La definizione del metodo procede poi nel solito modo:
void T::Paint() { // non bisogna mettere virtual
/* ... */
}
I metodi virtuali vengono ereditati allo stesso modo di quelli non virtual ,
possono anch'essi essere sottoposti a overloading ed essere ridefiniti, non
c'e` alcuna differenza eccetto che una loro invocazione non viene risolta se
non a run-time. Quando una classe possiede un metodo virtuale, il compilatore
associa alla classe (non all'istanza) una tabella (VTABLE ) che
contiene per ogni metodo virtuale l'indirizzo alla corrispondente funzione,
ogni istanza di quella classe conterra` poi al suo interno un puntatore (VPTR )
alla VTABLE ; una chiamata ad una funzione membro virtuale (e solo
alle funzioni virtuali) viene risolta con del codice che accede alla VTABLE
corrispondente al tipo dell'istanza tramite il puntatore contenuto nell'istanza
stessa, ottenuta la VTABLE invocare il metodo corretto e` semplice.
Le funzioni virtuali hanno il grande vantaggio di consentire l'aggiunta di nuove
classi alla gerarchia e di renderle immediatamente e correttamente utilizzabili
dal vostro programma senza doverne modificare il codice (ovviamente il programma
dovra` comunque essere modificato in modo che possa istanziare le nuove classi,
ma il codice che gestisce che lavorava sul generico supertipo non avra` bisogno
di modifiche), il late binding fara` in modo che siano chiamate sempre le funzioni
corrette senza che il vostro programma debba curarsi dell'effettivo tipo dell'istanza
che sta manipolando.
L'invocazione di un metodo virtuale e` piu` costosa di quella per una funzione
membro ordinaria, tuttavia il compilatore puo` evitare tale overhead risolvendo
a compile-time tutte quelle situazioni in cui il tipo e` effettivamente noto.
Ad esempio:
Td Obj1;
T* Ptr = 0;
/* ... */
Obj1.Paint(); // Chiamata risolvibile staticamente
Ptr->Paint(); // Questa invece no
La prima chiamata al metodo Paint() puo` essere risolta in fase
di compilazione perche` il tipo di Obj1 e` sicuramente Td ,
nel secondo caso invece non possiamo saperlo (anche se un compilatore intelligente
potrebbe cercare di restringere le possibilita` e, in caso di certezza assoluta,
risolvere staticamente la chiamata). Se poi volete avere il massimo controllo,
potete costringere il compilatore ad una "soluzione statica" utilizzando il
risolutore di scope:
Td Obj1;
T* Ptr = 0;
/* ... */
Obj1.Td::Paint(); // Chiamata risolta staticamente
Ptr->Td::Paint(); // ora anche questa.
Adesso sia nel primo che nel secondo caso, il metodo invocato e` Td::Paint() .
Fate attenzione pero` ad utilizzare questa possibilita` con i puntatori (come
nell'ultimo caso), se per caso il tipo corretto dell'istanza puntata non corrisponde,
potreste avere delle brutte sorprese.
Il meccanismo delle funzioni virtuali e` alla base del polimorfismo:
poiche` l'oggetto puntato da un puntatore puo` appartenere a tutta una gerarchia
di tipi, possiamo considerare l'istanza puntata come un qualcosa che puo` assumere
piu` forme (tipi) e comportarsi sempre nel modo migliore "recitando" di volta
in volta il ruolo corretto (da qui il soprannome di "Linguaggi degli attori"),
in realta` pero` un'istanza non puo` cambiare tipo, e solo il puntatore che
ha la possibilita` di puntare a tutta una gerarchia di classi.
|