La conversione statica viene effettuata a tempo di compilazione sfruttando il meccanismo di inferenza dei tipi del compilatore per determinare l'esistenza di una sequenza di conversioni esplicite e/o implicite adatta al caso d'uso.
Nel caso di una conversione tra tipi primitivi del linguaggio, effettuare un cast statico implica il fatto che il compilatore aggiunga delle istruzioni apposite per effettuare la conversione o la promozione da un tipo ad un altro, come nel listato seguente:
int main()
{
double a = 15.5;
int b = static_cast<int>(a); // stesso effetto di int b = a;
return 0;
}
In questo esempio, l'uso di static_cast
ha esattamente lo stesso effetto di una conversione implicita e si traduce nella sequenza di istruzioni assemly specializzate per effettuare la conversione di tipo. Ad esempio, usando il compilatore GCC 8.2 per architettura x86_64, l'assembly prodotto senza ottimizzazioni per il listato precedente è il seguente:
main:
push rbp
mov rbp, rsp
// inizializzazione di a
movsd xmm0, QWORD PTR .LC0[rip]
movsd QWORD PTR [rbp-8], xmm0
// conversione di a (double) in intero
movsd xmm0, QWORD PTR [rbp-8]
cvttsd2si eax, xmm0
// copia del valore intero (b)
// in un'altra locazione dello stack
mov DWORD PTR [rbp-12], eax
mov eax, 0
pop rbp
ret
.LC0:
.long 0 // la rappresentazione del letterale 15.5,
.long 1076822016 // strutturata in due word
In questo contesto, l'analisi del listato assembly è interessante poichè l'inoculazione di istruzioni specializzate a tempo di compilazione è una caratteristica del solo operatore di conversione statica. In virtù di questa peculiarità, le conversioni che riguardono tipi predefiniti o puntatori a tipi predefiniti sono protette dal meccanismo di inferenza del compilatore stesso, ed eventuali incompatibilità tra i tipi origine e destinazione sono determinate al momento della compilazione.
Ad esempio nel listato seguente, l'uso dell'operatore di conversione statica previene la conversione di un puntatore da double
a int
, generando un errore di compilazione.
int main()
{
double a = 15.5;
int* c = static_cast<int*>(&a); // errore!
void* d = static_cast<void*>(&a); // ok
return 0;
}
Unica eccezione a questa regola è la possibilità di effettuare una conversione statica da e verso il tipo void*
a partire da qualsiasi puntatore. Ciò è possibile poiché l'operazione di derefereziazione non si applica al tipo void*
e pertanto l'unico modo di accedere al valore da esso puntato consiste nell'effettuare ancora una volta una conversione ad un tipo diverso da void*
. Il compilatore quindi permette questa conversione poiché in questo caso si assume che sia responsabilità del programmatore assicurarsi che tale puntatore sia usato propriamente.
Cast statico e polimorfismo
Effettuare una conversione staticamente espone ad alcune criticità per quanto riguarda tutte le operazioni che coinvolgono oggetti polimorfici.
In particolare, il compilatore è ignaro del tipo effettivo di un oggetto quando esso viene referenziato polimorficamente a tempo di esecuzione. Tuttavia, le istruzioni necessarie ad effettuare una conversione di tipo sono inserite a tempo di compilazione.
Nel contesto di conversioni tra tipi per cui sussiste un vincolo di ereditarietà pubblica, si distinguono quindi due scenari in cui l'uso dell'operatore di conversione statica ha implicazioni differenti.
Upcasting
Per upcasting si intende la conversione di un puntatore o reference ad un oggetto di una classe derivata ad uno della classe base, includendo in questa definizione anche basi indirette. In pratica in senso gerarchico ci si sposta da un livello più specializzato ad uno più generico o astratto per manipolare la particolare istanza in uso.
Dato il layouting degli oggetti costruiti per ereditarietà, questo tipo di conversione si adatta ad essere effettuata staticamente. Infatti l'istanza di un oggetto derivato include sicuramente tutti i membri della classe base, anche quelli privati, e pertanto la derefereziazione di un puntatore ottenuto per mezzo di tale conversione non espone a rischi di violazione di accesso.
Downcasting
Per downcasting si intende una conversione opposta, cioè quando un puntatore o reference ad una classe base è convertito ad uno di classe derivata.
In questo caso, se il tipo effettivo dell'istanza puntata non corrisponde a quello di destinazione è possibile che l'uso del puntatore ottenuto per effetto della conversione degeneri in una condizione di violazione di accesso alla memoria. Pertanto in questo contesto, la correttezza nell'uso di conversioni statiche è a carico del programmatore.
Nel listato seguente ad esempio, si effettuano conversioni tra puntatori di classi affini sia in un senso che nell'altro. Lo scenario di downcasting mediante conversione statica può dar luogo a comportamenti inattesi del programma e pertanto dovrebbe essere evitato. Al suo posto è più corretto utilizzare l'operatore di conversione dinamica che tratteremo prossimamente.
class Base
{
public:
int a;
};
class Derived : public Base
{
public:
int b;
};
int main()
{
// upcasting (scenario sicuro)
Derived d;
Base *bptr = static_cast<Base*>(&d);
bptr->a = 10; // ok
// downcasting (scenario insicuro)
Base b;
Derived* dptr = static_cast<Derived*>(&b);
dptr->a = 20; // ok
dptr->b = 30; // errore! Possibile violazione di accesso
return 0;
}
In entrambi i casi è comunque opportuno osservare che la conversione statica applicata a reference o oggetti acceduti per valore comporta slicing, cioè una copia (o spostamento) parziale dell'oggetto in questione, poichè l'operatore di conversione statica in questo caso effettua la conversione richiamando il costruttore di copia (o di spostamento) dell'oggetto.
Tipi enum e conversioni statiche
L'uso di conversioni statiche è in generale molto efficace per quanto riguarda la conversione da e verso tipi enum con ambito di visibilità e valori numerici.
enum class MyEnum : unsigned char
{
first = 1,
second = 2,
third = 3
};
int main()
{
MyEnum val1 = static_cast<MyEnum>(2.5); // ok
MyEnum val2 = static_cast<MyEnum>(512); // comportamento indefinito
return 0;
}
La conversione statica consente di assegnare valori interi ad una variabile di tipo enum, tuttavia è onere del programmatore verificare che il valore in questione ricada nell'intervallo di valori rappresentati dalla enum. In caso contrario il valore assegnato alla variabile è indefinito. L'operatore di conversione statica selezionerà la sequenza di conversioni implicite ed esplicite appropriate per cercare di determinare il valore idoneo. Ad esempio, 2.5 è implicitamente convertito da double
a int
e poi a MyEnum::second
.
static_cast vs. l'operatore di conversione in stile C
L'implementazione dell'operatore di conversione in stile C, che abbiamo introdotto nella lezione precedente, si traduce nell'applicare in sequenza gli operatori di conversione del linguaggio C++, a partire da quelli con requisiti più stringenti. Il primo degli operatori di conversione che dà esito positivo è usato per effettuare la conversione, oppure, in caso di fallimento totale, viene emesso un errore di compilazione.
La conversione statica è tra i primi operatori ad essere applicati, quindi in molte situazioni una conversione statica produce il medesimo effetto di una conversione implicita o esplicita mediante operatore di conversione in stile C.
Tuttavia, quando si fa ricorso a quest'ultimo, se il compilatore determina che è impossibile effettuare una conversione statica tra due entità, non viene emesso alcun messaggio di errore e si procede con il prossimo operatore nella sequenza. Di conseguenza, quando il nostro intento è quello di garantire che una conversione sia predeterminata a tempo di compilazione, l'uso esplicito dell'operatore static_cast
, laddove applicabile, è preferibile come strumento di controllo e prevenzione degli errori di conversione.