Finora abbiamo esaminato la fenomenologia del polimorfismo in C++ senza però svelare i meccanismi implementativi che lo rendono possibile.
In questa lezione tratteremo alcuni aspetti tecnici che non sono strettamente necessari per la programmazione in C++, ma che tuttavia sono interessanti da conoscere ed incrementano la consapevolezza generale nell'uso di alcuni costrutti del linguaggio.
Sebbene l'implementazione del comportamento polimorfico degli oggetti dipenda fortemente dal compilatore utilizzato, nella maggior parte dei casi essa fa uso di una struttura di appoggio che viene creata automaticamente dal compilatore in ogni classe che contenga almeno un metodo virtuale, in modo del tutto trasparente per il programmatore.
Essa prende il nome di tabella dei metodi virtuali, o vtable, e contiene un elemento del tipo <nome_della_funzione, indirizzo_di_memoria> per ogni metodo virtuale della classe.
L'indirizzo di memoria in questione corrisponde alla locazione del blocco di istruzioni del metodo virtuale all'interno del segmento istruzioni della memoria riservata al programma.
La tabella dei metodi virtuali è generalmente inaccessibile per il programmatore, tuttavia in fase di esecuzione è proprio l'accesso a tale struttura che consente di ottenere l'associazione dinamica dei metodi ad un oggetto.
Il compilatore provvede ad inserire il codice necessario per allocare la tabella dei metodi virtuali ed aggiunge alla classe un puntatore a tale struttura, spesso indicato con il nome vptr.
La particolarità di tale membro consiste nel modo in cui esso è ereditato e gestito dalle classi derivate di modo da puntare sempre all'implementazione più specializzata di un metodo virtuale.
Il compilatore inietta nelle classi con metodi virtuali un codice apposito per l'inizializzazione del puntatore alla tabella dei metodi virtuali. In particolare, nel costruttore esso è inizializzato di modo da puntare ad una struttura statica, cioè che ha un ciclo di vita pari a tutta la durata del programma ed è unica per classe. Tutti gli oggetti dello stesso tipo infatti condividono l'accesso alla medesima tabella dei metodi virtuali.
Nel caso di classi che reimplementano i metodi virtuali derivati da una classe base, il costruttore, tra le altre cose, reinizializza il puntatore alla tabella dei metodi virtuali di modo che punti alla tabella corretta, quella cioè della classe derivata.
Questo meccanismo garantisce l'invocazione del metodo virtuale specializzato anche quando esso viene invocato usando un puntatore alla classe base.
La figura seguente mostra il layout dei segmenti di memoria di un programma in esecuzione in cui sono stati allocati due oggetti, Shape_Obj ed Ellipse_Obj di tipo Shape ed Ellipse, che nelle lezioni abbiamo definito rispettivamente come classe base e derivata.
Nella lezione precedente abbiamo aggiunto alla definizione di tali classi un metodo virtuale per il calcolo dell'area, pertanto tali oggetti conterranno un puntatore alle rispettive tabelle dei metodi virtuali, vptr, come illustrato in figura.
Come abbiamo accennato in precedenza, l'uso e la definizione della tabella dei metodi virtuali non sono prescritti dallo standard del linguaggio, pertanto la sua implementazione dipende dal compilatore usato.
Tuttavia, per lo scopo di questa lezione, supponiamo che essa sia allocata nel segmento dati della memoria riservata al programma, come del resto accade in alcune implementazioni reali.
L'oggetto Shape_Obj contiene un puntatore alla tabella dei metodi virtuali propria per la classe Shape. In essa l'indirizzo di memoria associato al metodo area()
punta all'implementazione specifica della classe Shape, che supponiamo essere localizzata all'indirizzo 0xAAAAAA del segmento istruzioni della memoria del programma.
Poichè la classe Ellipse include una reimplementazione specializzata del metodo area(), il puntatore vptr dell'oggetto Ellipse_Obj, che è contenuto nella sezione dei dati derivati dalla classe base, viene reinizializzato dal costruttore di modo da puntare alla tabella propria degli oggetti di tipo Ellipse, anch'essa contenuta nel segmento dati.
Tuttavia in questa seconda struttura, l'indirizzo di memoria del metodo area() punta all'implementazione specifica della classe Ellipse, che supponiamo essere localizzata all'indirizzo 0xBBBBBB del segmento istruzioni.
Questo modo di gestire i riferimenti ai metodi virtuali fa sì che quando un oggetto di tipo derivato viene referenziato usando un puntatore alla classe base, in osservanza ai principi del polimorfismo, l'invocazione di un metodo virtuale selezioni l'implementazione più specifica disponibile per quella classe.
La tabella dei metodi virtuali di una classe derivata contiene riferimenti ai metodi della classe base solo quando per essi non viene fornita un'implementazione specializzata. In questo modo è garantito il riutilizzo di codice con valenza generale nel nostro modello di dati.