Limiti e regole del polimorfismo
Esistono limitazioni e regole connesse all'uso delle funzioni virtuali legate
piu` o meno direttamente al funzionamento del polimorfismo; vediamole in dettaglio.
Una funzione membro virtuale non puo` essere dichiarata static :
i metodi virtuali sono fortemente legati alle istanze, mentre i metodi statici
sono legati alle classi.
Se una classe base dichiara un metodo virtual , tale metodo restera`
sempre virtual in tutte le classi derivate anche quando la classe
derivata lo ridefinisce senza dichiararlo esplicitamente virtual .
Scrivere cioe`
class Base {
public:
virtual void Foo();
};
class Derived: public Base {
public:
void Foo();
};
e` lo stesso che scrivere
class Base {
public:
virtual void Foo();
};
class Derived: public Base {
public:
virtual void Foo();
};
Entrambe le forme sono lecite, possiamo cioe` omettere virtual
all'interno delle classi derivate quando vogliamo ridefinire un metodo ereditato.
E` possibile che una classe derivata ridefinisca virtual un membro
che non lo era nella classe base. In questo caso il comportamento del compilatore
puo` sembrare strano; vediamo un esempio:
#include < iostream >
using std::cout;
class Base {
public:
void Foo() {
cout << "Base::Foo()" << endl;
}
};
class Derived1: public Base{
public:
virtual void Foo(){
cout << "Derived1::Foo()" << endl;
}
};
class Derived2: public Derived1 {
public:
virtual void Foo(){
cout << "Derived2::Foo()" << endl;
}
};
int main(int, char* []) {
Base* BasePtr = new Derived1;
Derived* DerivedPtr = new Derived2;
BasePtr -> Foo();
DerivedPtr -> Foo();
return 0;
}
Eseguendo il precedente codice, otterreste questo output:
Base::Foo()
Derived2::Foo()
Essendo BasePtr un puntatore alla classe base, il compilatore non
puo` forzare il late binding perche` l'istanza puntata potrebbe essere effettivamente
di tipo Base ; per non rischiare una operazione che potrebbe mandare
in crash il sistema, il compilatore adotta un approccio conservativo e chiama
sempre Base::Foo() (al piu` il programma non fara` la cosa giusta,
ma l'integrita` del resto del sistema non sara` compromessa). Nel secondo caso,
poiche` DerivedPtr puo` puntare solo a istanze di Derived1
o sue sottoclassi, viene eseguito regolarmente il late binding perche` il metodo
Foo() sara` sempre virtual .
Da questo comportamento deriva un suggerimento ben preciso: se una classe potrebbe
in futuro essere derivata (se pensate cioe` che ci siano validi motivi per specializzarla
ulteriormente), e` bene che i metodi dell'interfaccia (potenzialmente soggetti
a ridefinizione) siano sempre virtual , onde evitare potenziali
errori; d'altronde se non si usa il polimorfismo e` comunque possibile forzare
la risoluzione del binding a compile time.
Non e` possibile avere costruttori virtuali. Il meccanismo che sta alla base
del late binding richiede che l'istanza che si sta creando sia correttamente
collegata alla sua VTABLE , ma il compito di settare correttamente
il puntatore (VPTR ) alla VTABLE spetta proprio al
costruttore che di conseguenza non puo` essere dichiarato virtual .
Questo problema avviamente non esiste per i distruttori, anzi e` bene che una
classe che possieda metodi virtuali abbia anche il distruttore virtual
in modo che distruggendo oggetti polimorfi venga sempre invocato il distruttore
corretto.
Metodi virtuali chiamati dentro il costruttore o nel distruttore sono sempre
risolti staticamente. Il motivo e` semplice e riconducibile all'ordine in cui
sono chiamati costruttori e distruttori.
Consideriamo il seguente esempio:
class Base {
public:
Base();
~Base();
virtual void Foo();
};
Base::Base() {
Foo();
}
Base::~Base() {
Foo();
}
void Base::Foo() {
/* ... */
}
class Derived: public Base {
public:
virtual void Foo();
};
void Derived::Foo() {
/* ... */
}
La costruzione di un oggetto Derived richiede che sia prima eseguito
il costruttore di Base . Al momento in cui viene eseguita la chiamata
a Foo() contenuta nel costruttore della classe base il puntatore
alla VTABLE puo` al piu` puntare alla VTABLE della classe Base
perche` il costruttore della classe derivata non e` ancora stato eseguito e
non e` quindi possibile chiamare Derived::Foo() , si puo` chiamare
solo la versione locale di Foo() . La situazione e` analoga nel
caso dei distruttori, al momento in cui viene eseguito il distruttore della
classe bae, il distruttore della classe derivata e` gia` stato eseguito ed il
puntatore alla VTABLE non e` piu` valido; di conseguenza si puo` invocare
solo Base::Foo() .
Il suggerimento e` quindi quello di evitare per quanto possibile la chiamata
di metodi virtuali all'interno di costruttori e distruttori, il risultato che
potreste ottenere molto probabilmente non sarebbe quello desiderato.
Un potenziale errore legato all'uso di funzioni virtuali e` possibile quando
una funzione membro richiama un'altra funzione membro virtuale. Consideriamo
questo frammento di codice:
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 non virtuale (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.
Il risultato in queste situazioni e` che il comportamento che una classe puo`
avere e` molto difficile da controllare e potrebbe essere potenzialmente errato;
l'errore legato a situazioni di questo tipo e` noto in letteratura come fragile
class problem e` puo` essere causa di forti inconsistenze.
Il polimorfismo e` uno strumento estremamente potente, tuttavia richiede una
approfondita comprensione del suo funzionamente per essere utilizzato correttamente
e in modo profiquo.
|