La conversione di tipo dinamica si presta al caso di oggetti polimorfici, in particolare, quando non vi è cognizione del tipo effettivo di un'istanza, eccetto per il fatto che esso appartiene ad una specifica gerarchia.
In molti scenari, infatti, il tipo di un'istanza non può essere dedotto a tempo di compilazione poiché è determinato durante l'esecuzione del programma, in modo impredicibile. In questi casi la conversione dinamica è lo strumento che il programmatore ha a disposizione per determinare se il tipo di un oggetto corrisponde al tipo atteso in un determinato contesto, e di intraprendere azioni in base all'esito di questo confronto.
Il supporto alle conversioni dinamiche non è una caratteristica nativa del linguaggio. Essa è implementata mediante l'uso di un apposito sottosistema di gestione dei metadati delle classi, la cui implementazione è trasparente al programmatore, e varia in base al compilatore in uso.
In virtù di tali considerazioni, è bene essere a conoscenza del fatto che esistono dei prerequisiti ed alcune limitazioni nell'uso di tale metodo di conversione.
Run-Time Type Information
L'operatore di conversione dinamica è parte dei costrutti sintattici offerti dal sottosistema RTTI (Run-Time Type Information) del linguaggio C++. Esso consente di estendere le capacità di controllo dei tipi anche a tempo di esecuzione.
Il C++ è un linguaggio caratterizzato da tipizzazione forte ed un uso pervasivo del controllo statico dei tipi. L'uso corretto dei tipi è infatti una caratteristica imprescindibile per il funzionamento di un programma in C++, in quando non sono ammesse ambiguità nella definizione di un oggetto e dei comportamenti ad esso associati.
L'unica eccezione a questa regola, come abbiamo visto in precedenza, si estrinseca nell'uso del polimorfismo per la gestione di oggetti che condividono una comune interfaccia di programmazione, con comportamenti specializzati in base alle esigenze funzionali della nostra applicazione.
L'operatore di conversione dinamica trova applicazione proprio in questo contesto, e consente di discernere il tipo effettivo di un'istanza quando essa viene acceduta mediante dereferenziazione di un puntatore o reference.
Ad esempio, nel listato seguente è presente una gerarchia minimale, composta da una classe base e da una sua diretta derivata. Nel corpo della funzione main()
vengono eseguite delle istanziazioni e conversioni che mostrano l'efficacia dell'operatore dynamic_cast
(case 1 e 2) e le sue differenze rispetto una conversione esplicita effettuata mediante l'operatore di conversione in stile C (case 3).
#include <iostream>
class Base
{
public:
Base() {};
virtual ~Base(){};
};
class Derived : public Base
{
public:
Derived() {};
~Derived() {};
void doStuff(int number)
{
std::cout << "doing stuff with number " << number << "...\n";
}
};
int main()
{
// case 1
Base* bptr1 = new Derived();
Derived* dptr = dynamic_cast<Derived*>(bptr1);
if (dptr != nullptr)
dptr->doStuff(1);
// case 2
Base* bptr2 = new Base();
Derived* dptr2 = dynamic_cast<Derived*>(bptr2);
if (dptr2 != nullptr)
dptr2->doStuff(2);
// case 3
dptr2 = (Derived*) bptr2;
if (dptr2 != nullptr)
dptr2->doStuff(3); // comportamento indefinito!
return 0;
}
Nel primo caso, un'istanza della classe derivata è referenziata polimorficamente mediante un puntatore alla classe base. L'operatore di conversione dinamica è usato per convertire tale puntatore al tipo della classe derivata, per consentire l'invocazione di un metodo proprio della classe derivata. In questo caso poiché il tipo dell'istanza puntata ed il tipo del puntatore di destinazione coincidono, la conversione dà esito positivo.
Il secondo caso è molto simile al primo ma, questa volta, il tipo dell'istanza puntata è quello della classe base. L'operatore di conversione dinamica quindi produce come risultato un puntatore nullo, poiché consentire un downcasting in questo scenario potrebbe dare origine a effetti indesiderati.
Il valore restituito dall'operatore di conversione dinamica quindi è un puntatore valido solo se il tipo di destinazine corrisponde a quello dell'istanza oggetto di conversione, mentre è nullo in caso opposto. Per questo motivo è sempre necessario verificare l'esito di una conversione dinamica mediante l'uso di istruzioni condizionali.
Nel terzo ed ultimo caso invece, si ripropone il medesimo problema di downcasting insicuro ma, questa volta, con l'uso dell'operatore di conversione in stile C. A differenza dell'operatore di conversione dinamica, esso si traduce in una conversione statica che, in questo scenario, risulta non appropriata. Di consequenza, l'effetto dell'invocazione del metodo doStuff(), che non è proprio dell'istanza referenziata da dptr2, è indefinito.
Prerequisiti
Fin qui abbiamo analizzato il comportamento dell'operatore di conversione dinamica da un punto di vista prettamente funzionale. In realtà la sua applicabilità è soggetta ad almeno due fattori imprescindibili:
- Polimorfismo, non semplice ereditarietà: i tipi su cui opera
dynamic_cast
devono essere, polimorfici, cioè dotati di almeno un metodo virtuale. In questo esempio l'unico metodo virtuale è il distruttore della classe base, che come abbiamo già discusso in precedenza, è necessario per garantire la corretta deallocazione degli oggetti della gerarchia quando essi vengono referenziati in modo polimorfico.
La presenza di metodi virtuali è il fattore abilitante per l'inclusione della tabella dei metodi virtuali nella definizione della classe da parte del compilatore. Le informazioni relative al tipo che vengono gestite tramite RTTI sono infatti residenti in questa tabella, la cui presenza è quindi imprescindibile per operare conversioni dinamiche. - Supporto a RTTI abilitato durante la compilazione: RTTI è un sottosistema del run-time di C++ che può essere disabilitato per ragioni di sicurezza o performance, ad esempio per applicazioni destinate a sistemi embedded. Esso è normalmente abilitato per default dalla maggior parte dei compilatori.
La mancanza di uno o di entrambi questi requisiti verrà segnalata dal compilatore mediante l'emissione di messaggi di errore.
Problematiche relative alla compilazione e al linking
Il supporto a RTTI deve essere esteso ad ogni componente della nostra applicazione per abilitare l'uso dell'operatore di conversione dinamica tra istanze la cui definizione risiede in file oggetto differenti, ad esempio quando la nostra applicazione si articola su più librerie.
Inoltre, poiché il linker tende a segregare tutti i simboli definiti internamente ad ogni una unità di traduzione, l'uso di librerie condivise e caricate dinamicamente può compromettere la funzionalità dell'operatore di conversione dinamica. Pertanto può essere necessario attivare opportune opzioni di compilazione e linking per garantire che il processo di compilazione sia consistente con RTTI.