Perché l'ereditarietà da sola non basta
Nella pagina precedente hai costruito una gerarchia di classi: una classe derivata eredita i membri dalla sua base. Ma c'è un problema. Quando chiami un metodo tramite un puntatore alla classe base, il C++ decide quale funzione eseguire in base al tipo del puntatore, non al tipo reale dell'oggetto. Così un Animal* che in realtà punta a un Dog chiama comunque la versione del metodo di Animal.
Non è quasi mai ciò che vuoi. Di solito hai una raccolta di puntatori alla classe base, ognuno dei quali punta a un oggetto derivato diverso, e vuoi che ognuno si comporti per quello che è. Le funzioni virtuali rendono possibile proprio questo.
L'oggetto è davvero un Dog, eppure a->speak() ha eseguito Animal::speak(). Dato che speak non è virtuale, il compilatore ha scelto la funzione in fase di compilazione in base al tipo statico Animal*. Questo è il bug che le funzioni virtuali esistono per correggere.
Rendere virtuale una funzione
Aggiungi la parola chiave virtual al metodo della classe base. Ora la chiamata viene risolta a runtime in base al tipo reale dell'oggetto: questo è il dispatch dinamico.
Un solo ciclo su Animal*, tre comportamenti diversi. Il puntatore alla base "conosce" il tipo reale a runtime e smista la chiamata di conseguenza. Questo singolo meccanismo, un'interfaccia e tante implementazioni, è ciò che significa polimorfismo in C++.
Nota che virtual serve solo nella dichiarazione della base; una volta che una funzione è virtuale, resta virtuale in automatico in ogni classe derivata. Riscriverlo nella classe derivata è facoltativo e ridondante.
Usa sempre la parola chiave override
Nell'esempio sopra, ogni metodo derivato è marcato override. Non è necessario perché il codice funzioni, ma dovresti considerarlo obbligatorio. override (C++11) chiede al compilatore di verificare che tu stia davvero ridefinendo una funzione virtuale della base con una firma corrispondente. Se sbagli la firma di poco, ottieni un errore chiaro invece di un bug silenzioso.
struct Animal {
virtual void speak() const { } // nota: const
};
struct Dog : Animal {
void speak() { } // NON const: questa è una funzione NUOVA, non un override!
void speak() override { } // errore: 'speak' non fa override, te lo dice subito
};
Senza override, il primo speak() compila senza problemi ma non viene mai chiamato tramite un Animal*, perché la sua firma è diversa da quella della base (manca const). Passeresti un pomeriggio a chiederti perché il tuo override non fa nulla. Con override, il compilatore scopre subito la discrepanza. Aggiungilo a ogni funzione che fa override.
Funzioni virtuali pure e classi astratte
A volte la classe base non ha un comportamento predefinito sensato: che verso fa un generico "Animal"? In quel caso dichiara la funzione virtuale pura assegnandole = 0. Così non ha corpo e la classe diventa una classe astratta, che non può essere istanziata da sola. Esiste solo per definire un'interfaccia che le classi derivate devono rispettare.
Ogni sottoclasse concreta deve implementare area(), altrimenti resta astratta anche lei. È così che il C++ esprime le "interfacce": una classe astratta con sole funzioni virtuali pure è l'equivalente C++ di un'interfaccia in linguaggi come Java.
La regola del distruttore virtuale
Questa è l'insidia che colpisce tutti almeno una volta. Quando fai delete di un oggetto tramite un puntatore alla classe base, il C++ chiama il distruttore che trova, e se quel distruttore non è virtuale esegue solo il distruttore della base. La parte derivata non viene mai distrutta, e tutto ciò che possedeva va perso. Lo standard lo definisce undefined behavior.
La soluzione è una parola: rendi virtual il distruttore della base. Allora delete p esegue prima ~Derived e poi ~Base, esattamente come deve.
struct Base {
virtual ~Base() { cout << "~Base\n"; } // corretto
};
// ora: ~Derived e poi ~Base
Regola pratica: nel momento in cui una classe ha una qualsiasi funzione virtuale, dalle anche un distruttore virtuale. Se una classe è pensata per fare da base usata tramite puntatori, il suo distruttore deve essere virtuale.
Errori comuni e insidie
Qualche altra trappola da tenere d'occhio quando ti sarai abituato alle funzioni virtuali:
Object slicing. Se passi o memorizzi un oggetto derivato per valore in una variabile di tipo base, la parte derivata viene "tagliata via" e ti resta un semplice oggetto base: il dispatch virtuale non raggiunge più l'override. Per il polimorfismo usa sempre puntatori o riferimenti:
Dog d;
Animal a = d; // TAGLIATO: ora a è solo un Animal, la parte Dog è sparita
a.speak(); // esegue Animal::speak anche se è virtuale
Animal& ref = d; // OK: il riferimento conserva il tipo reale
ref.speak(); // esegue Dog::speak
Non chiamare funzioni virtuali da costruttori o distruttori. Durante la costruzione la parte derivata non esiste ancora, quindi una chiamata virtuale si risolve nella versione della classe corrente, non nell'override della derivata: quasi mai ciò che intendi.
Il dispatch virtuale ha un piccolo costo. Ogni chiamata virtuale passa da una tabella nascosta di puntatori a funzione (la "vtable"), con un'indirezione per chiamata. Costa poco, ma non è gratis, quindi non rendere virtuale una funzione se non ti serve davvero ridefinirla.
Chiamare di proposito la versione della base. Dentro un override puoi comunque invocare esplicitamente l'implementazione della base con Base::method(): utile quando il comportamento derivato estende quello della base invece di sostituirlo.
Prossimo: overloading degli operatori
Le funzioni virtuali permettono ai tuoi oggetti di personalizzare il loro comportamento attraverso un'interfaccia comune. La prossima pagina mostra come personalizzare gli operatori che agiscono sui tuoi oggetti: con l'overloading degli operatori puoi insegnare ai tuoi tipi a rispondere a +, ==, << e altri ancora, così che Vector + Vector o cout << myObject si leggano in modo naturale come per i tipi predefiniti.
Domande frequenti
Cos'è una funzione virtuale in C++?
Una funzione virtuale è una funzione membro dichiarata con la parola chiave virtual in una classe base, in modo che, quando la chiami tramite un puntatore o un riferimento alla classe base, il C++ esegua l'override della classe derivata invece della versione base. Questa scelta a runtime si chiama dispatch dinamico ed è alla base del polimorfismo.
Qual è la differenza tra una funzione virtuale e una funzione virtuale pura?
Una funzione virtuale ha un corpo e può essere ridefinita. Una funzione virtuale pura si dichiara con = 0 e non ha corpo nella classe base: obbliga ogni classe derivata concreta a fornire un'implementazione. Qualsiasi classe con almeno una funzione virtuale pura è una classe astratta e non può essere istanziata.
Perché una classe base ha bisogno di un distruttore virtuale in C++?
Se fai delete di un oggetto derivato tramite un puntatore alla classe base e il distruttore della base non è virtuale, viene eseguito solo il distruttore della base: la parte derivata non viene mai ripulita, il che fa perdere risorse ed è undefined behavior. Rendi virtual il distruttore di ogni classe pensata per essere usata in modo polimorfico.