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", tale
caratteristica consiste nella possibilita` di rimandare a tempo di esecuzione il
linking di una o piu` funzioni membro (late-binding), 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 ne errori ne warning.
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(); default : /* errore */ };Una soluzione di questo tipo funziona ma e` macchinosa, allunga il lavoro, una dimenticanza puo` costare cara e soprattutto ogni volta che si modifica la gerarchia di classi bisogna modificare anche il codice che la usa.
class T { public: /* ... */ virtual void Paint(); private: /* ... */ };La definizione del metodo procede poi nel solito modo:
void T::Paint() { // non occorre mettere virtual /* ... */ }I metodi virtuali vengono ereditati allo stesso modo di quelli "normali" (o meglio statici), 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 che contiene per ogni metodo virtuale l'indirizzo alla corrispondente funzione, ogni istanza di quella classe conterra` poi al suo interno un puntatore alla tabella; una chiamata ad una funzione membro virtuale (e solo alle funzioni virtuali) viene risolta con del codice che accede alla tabella corrispondente al tipo dell'istanza tramite il puntatore contenuto nell'istanza stessa, ottenuta la tabella invocare il metodo corretto e` semplice.
Td Obj1; T * Ptr = 0; /* ... */ Obj1.Paint(); // Chiamata risolvibile staticamente Ptr->Paint(); // Questa invece noLa 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.
class T { public: virtual void Foo(); virtual void Foo2(); void DoSomething(); private: /* ... */ }; /* implementazione di T::Foo() e T::Foo2() */ void T::DoSomething() { /* ... */ Foo(); /* ... */ Foo2(); /* ... */ } class Td : public T { public: virtual void Foo2(); void DoSomething(); private: /* ... */ }; /* implementazione di Td::Foo2() */ void Td::DoSomething() { /* ... */ Foo(); // attenzione chiama T::Foo() /* ... */ Foo2(); /* ... */ }Si tratta di una situazione pericolosa: la classe Td ridefinisce un metodo statico (ma poteva anche essere virtuale), ma non uno virtuale da questo richiamato. Di per se non si tratta di un errore, la classe derivata potrebbe non aver alcun motivo per ridefinire il metodo ereditato, tuttavia puo` essere difficile capire cosa esattamente faccia il metodo Td::DoSomething(), soprattutto in un caso simile:
class Td2 : public Td { public: virtual void Foo(); private: /* ... */ };Questa nuova classe ridefinisce un metodo virtuale, ma non quello che lo chiama, per cui in una situazione del tipo:
Td2 * Ptr = new Td2; /* ... */ Ptr->DoSomething();viene chiamato il metodo Td::DoSomething() ereditato, ma in effetti questo poi chiama Td2::Foo() per via del linking dinamico. Consiglio vivamente di riflettere sull'evoluzione di una esecuzione di funzione che chiami funzioni virtuali, solo in questo modo si apprendono vantaggi e pericoli derivanti dall'uso di funzioni virtuali.
class TShape { public: virtual void Paint() = 0; virtual void Erase() = 0; /* ... */ };Notate l'assegnamento effettuato alle funzioni virtuali, funzioni di questo tipo vengono dette funzioni virtuali pure e l'assegnamento ha il compito di informare il compilatore che non intendiamo definire i metodi virtuali. Una classe che possiede funzioni virtuali pure e detta classe astratta e non e` possibile istanziarla; essa puo` essere utilizzata unicamente per derivare nuove classi forzandole a fornire determinati metodi (quelli corrispondenti alle funzioni virtuali pure). Il compito di una classe astratta e` quella di fornire una interfaccia senza esporre dettagli implementativi. Se una classe derivata da una classe astratta non implementa una qualche funzione virtuale pura, diviene essa stessa una classe astratta.
class TShape { public: virtual void Paint() = 0; // ogni figura puo` essere virtual void Erase() = 0; // disegnata e cancellata! }; class TPoint : public TShape { public: TPoint(int x, int y) : X(x), Y(y) {} private: int X, Y; // coordinate del punto }; void TPoint::Paint() { /* ... */ } void TPoint::Erase() { /* ... */ }Non e` possibile creare istanze della classe TShape, ma la classe TPoint ridefinisce tutte le funzioni virtuali pure e puo` essere istanziata e utilizzata dal programma; la classe TShape e` comunque ancora utile al programma, perche` possiamo dichiarare puntatori di tale tipo per gestire una lista di figure.