Menu

Type casting in C++: static_cast, conversioni implicite e cast

Come funziona il type casting in C++: conversioni implicite, la trappola della divisione intera e i quattro cast con nome (static_cast, const_cast, reinterpret_cast, dynamic_cast), con le insidie che causano perdite di dati silenziose.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Cos'è il type casting

Fare type casting significa convertire un valore da un tipo a un altro: trasformare un double in un int, un char nel suo codice numerico, o un puntatore alla classe base in uno alla classe derivata. Il C++ fa alcune di queste conversioni al posto tuo in automatico, ma quelle che fa in silenzio sono proprio dove si nascondono i bug.

Ne esistono due tipi: le conversioni implicite, che avvengono da sole, e i cast espliciti, che scrivi tu. Hai già incontrato un sintomo di tutto questo negli operatori: la divisione intera. Il casting è il modo per prenderne il controllo.

Conversioni implicite

Quando mescoli tipi numerici in un'espressione, il C++ promuove il tipo "più piccolo" a quello "più grande", così che entrambi i lati coincidano. Di solito fa quello che vuoi.

I guai iniziano quando la conversione va nella direzione opposta, da un tipo più ampio a uno più stretto. Questa è una conversione narrowing (restringimento), e può perdere dati in silenzio.

La conversione da float a int tronca verso lo zero: non arrotonda, quindi 3.99 diventa 3. E infilare 300 in un char va in overflow. Molti compilatori qui danno un avviso, altri no. Quando vuoi davvero restringere, dillo in modo esplicito con un cast, così chi legge dopo sa che era voluto.

Risolvere la trappola della divisione intera

Il motivo più comune per fare un cast è la divisione. Quando entrambi gli operandi sono interi, / esegue la divisione intera e butta via il resto.

La soluzione è static_cast<double> su un operando prima della divisione. Un errore frequente è static_cast<double>(got / total): è troppo tardi, perché quando il cast viene eseguito got / total vale già 0, quindi ottieni 0.0. Converti un operando, non il risultato.

static_cast: il cast da usare di default

Il C++ ti offre quattro cast con nome. Quello che userai il 95% delle volte è static_cast<T>(value), che esegue conversioni ben definite tra tipi correlati: conversioni numeriche, da enum a int, da void* di nuovo a un puntatore tipizzato, e su o giù per una gerarchia di classi quando conosci già il tipo.

Preferisci static_cast al vecchio cast in stile C (int)balance. Un cast in stile C proverà qualsiasi conversione pur di far compilare il codice, comprese quelle pericolose che vedi più sotto, quindi può togliere il const o reinterpretare i byte grezzi senza dirti nulla. static_cast permette solo le conversioni che il compilatore può davvero giustificare, e il prolisso static_cast<...> è facilissimo da cercare durante una code review.

// Da evitare: cast in stile C, senza rete di sicurezza:
int dollars = (int) balance;

// Da preferire: esplicito, controllato, facile da cercare:
int dollars = static_cast<int>(balance);

Gli altri tre cast (da usare con parsimonia)

Gli altri cast esistono per compiti specifici e limitati. Usali solo quando static_cast proprio non basta.

const_cast toglie (o aggiunge) const. Il suo unico uso legittimo è chiamare un'API in stile C che si è dimenticata di marcare un parametro come const. Modificare tramite un const_cast un oggetto dichiarato originariamente const è undefined behavior.

void legacyApi(char* msg);   // API vecchia, non accetta const

const char* text = "hello";
legacyApi(const_cast<char*>(text));   // va bene solo se legacyApi non ci scrive

reinterpret_cast reinterpreta la sequenza di bit grezza, ad esempio un puntatore come indirizzo intero. Non esegue alcuna conversione ed è estremamente insicuro; è quasi sempre il segnale che dovresti ripensare il design.

dynamic_cast converte in modo sicuro un puntatore o un riferimento alla classe base verso un tipo derivato a runtime, usando il tipo reale dell'oggetto. Richiede una base polimorfica (una classe con almeno una funzione virtuale) e restituisce nullptr se il cast non è applicabile.

Se a avesse puntato a un altro Animal, dynamic_cast<Dog*> avrebbe restituito nullptr e sarebbe stato eseguito il ramo else: ecco esattamente perché è più sicuro che usare alla cieca static_cast per scendere in una gerarchia.

Errori comuni da evitare

  • Convertire il risultato invece di un operando. static_cast<double>(a / b) elimina prima la parte decimale. Converti a o b.
  • Pensare che da float a int si arrotondi. Si tronca: static_cast<int>(2.99) vale 2. Per arrotondare usa std::round, std::lround e simili.
  • Ricorrere a un cast in stile C. Nasconde quale conversione avviene. Usa static_cast e otterrai un errore di compilazione quando la conversione non è sicura, invece di una sorpresa silenziosa.
  • Restringere in un tipo troppo piccolo. Convertire 300 in char o un long enorme in int ricomincia da capo o va in overflow. Scegli un tipo di destinazione abbastanza ampio per l'intervallo.

Prossimo: if-else

Ora che sai convertire e confrontare i valori in modo pulito, il passo successivo è prendere decisioni con essi. L'istruzione if-else esegue codice diverso a seconda che una condizione sia true: la base di ogni programma con delle diramazioni.

Domande frequenti

Qual è la differenza tra static_cast e un cast in stile C in C++?

Un cast in stile C come (int)x prova una conversione dopo l'altra: può trasformarsi in silenzio in un pericoloso reinterpret_cast o togliere il const. static_cast<int>(x) esegue solo conversioni correlate che il compilatore può verificare, quindi il compilatore rifiuta quelle senza senso. Nel C++ moderno preferisci sempre static_cast ai cast in stile C: è più sicuro e molto più facile da cercare con grep.

Come si converte un int in double in C++?

Usa static_cast<double>(x). Conta soprattutto nella divisione: 5 / 2 è una divisione intera e dà 2, mentre static_cast<double>(5) / 2 dà 2.5. Converti un operando prima che avvenga la divisione: convertire il risultato, static_cast<double>(5 / 2), è troppo tardi e dà comunque 2.0.

Perché convertire un valore grande in un tipo più piccolo dà un numero sbagliato in C++?

Convertire in un tipo che non può contenere il valore è una conversione narrowing (restringimento). Da float a int la parte decimale viene troncata (static_cast<int>(3.99) vale 3), e un intero fuori intervallo ricomincia da capo (unsigned) oppure dipende dall'implementazione (signed). Di solito il compilatore non ti ferma, quindi fai i cast in modo consapevole e assicurati che il tipo di destinazione sia abbastanza ampio.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA