Conversioni di Tipo in C++

Concetti Chiave
  • Le conversioni di tipo in C++ consentono di trasformare un valore di un tipo in un valore di un altro tipo.
  • Le conversioni implicite avvengono automaticamente in determinate circostanze, come nelle espressioni aritmetiche, nelle inizializzazioni e nelle condizioni.
  • Le conversioni aritmetiche seguono regole specifiche per preservare la precisione, promuovendo i tipi più piccoli a tipi più grandi quando necessario.
  • Le conversioni di puntatori includono la conversione di array in puntatori, la conversione di puntatori a void, e la conversione di puntatori a tipi const.
  • Le conversioni esplicite possono essere effettuate utilizzando cast nominati come static_cast, const_cast, e reinterpret_cast.
  • I cast in vecchio stile sono meno visibili e più difficili da rintracciare rispetto ai cast nominati, e il loro uso è generalmente sconsigliato.

Conversioni di Tipo

In C++ alcuni tipi sono correlati tra loro. Quando due tipi sono correlati, possiamo usare un oggetto o valore di un tipo dove è previsto un operando del tipo correlato. Due tipi sono correlati se esiste una conversione tra di loro. L'esempio classico è la conversione da un tipo in virgola mobile a un tipo intero e viceversa.

Come esempio, considera la seguente espressione, che inizializza ival, un intero, a 6:

// il compilatore potrebbe avvisare della perdita di precisione
int ival = 3.541 + 3;

Gli operandi dell'addizione sono valori di due tipi diversi: 3.541 ha tipo double, e 3 è un int. Piuttosto che tentare di sommare valori dei due tipi diversi, C++ definisce un insieme di conversioni per trasformare gli operandi in un tipo comune. Queste conversioni vengono eseguite automaticamente senza intervento del programmatore e talvolta senza che il programmatore ne sia a conoscenza. Per questa ragione, sono chiamate conversioni implicite.

Le conversioni implicite tra i tipi aritmetici sono definite per preservare la precisione, se possibile. Molto spesso, se un'espressione ha sia operandi interi che in virgola mobile, l'intero viene convertito in virgola mobile. In questo caso, 3 viene convertito in double, viene eseguita l'addizione in virgola mobile, e il risultato è un double.

L'inizializzazione avviene successivamente. In un'inizializzazione, il tipo dell'oggetto che stiamo inizializzando domina. L'inizializzatore viene convertito nel tipo dell'oggetto. In questo caso, il risultato double dell'addizione viene convertito in int e usato per inizializzare ival. Convertire un double in un int tronca il valore del double, scartando la parte decimale. In questa espressione, il valore 6 viene assegnato a ival.

Quando Avvengono le Conversioni Implicite

Il compilatore converte automaticamente gli operandi nelle seguenti circostanze:

  • Nella maggior parte delle espressioni, i valori di tipi interi più piccoli di int vengono prima promossi a un tipo intero più grande appropriato.
  • Nelle condizioni, le espressioni non bool vengono convertite in bool.
  • Nelle inizializzazioni, l'inizializzatore viene convertito nel tipo della variabile; negli assegnamenti, l'operando destro viene convertito nel tipo del sinistro.
  • Nelle espressioni aritmetiche e relazionali con operandi di tipi misti, i tipi vengono convertiti in un tipo comune.
  • Come vedremo nelle prossime lezioni, le conversioni avvengono anche durante le chiamate di funzione.

Le Conversioni Aritmetiche

Le conversioni aritmetiche, che abbiamo introdotto nelle lezioni precedenti, convertono un tipo aritmetico in un altro. Le regole definiscono una gerarchia di conversioni di tipo in cui gli operandi di un operatore vengono convertiti nel tipo più ampio. Ad esempio, se un operando è di tipo long double, allora l'altro operando viene convertito in tipo long double indipendentemente da quale sia il secondo tipo. Più in generale, nelle espressioni che mischiano valori in virgola mobile e interi, il valore intero viene convertito in un tipo appropriato in virgola mobile.

Promozioni intere

Le promozioni intere (a volte chiamate promozioni integrali) convertono i piccoli tipi interi in un tipo intero più grande. I tipi bool, char, signed char, unsigned char, short e unsigned short vengono promossi a int se tutti i valori possibili di quel tipo possono essere memorizzati in un int. Altrimenti, il valore viene promosso a unsigned int. Come abbiamo visto molte volte, un bool che è false viene promosso a 0 e true a 1.

I tipi char più grandi (wchar_t, char16_t e char32_t) vengono promossi al tipo più piccolo tra int, unsigned int, long, unsigned long, long long o unsigned long long in cui tutti i valori possibili di quel tipo carattere possono essere rappresentati.

Operandi di Tipo Unsigned

Se gli operandi di un operatore hanno tipi differenti, quegli operandi vengono normalmente convertiti in un tipo comune. Se un qualsiasi operando è un tipo unsigned, il tipo a cui gli operandi vengono convertiti dipende dalle dimensioni relative dei tipi interi sulla macchina.

Come al solito, le promozioni intere avvengono prima. Se i tipi risultanti corrispondono, non è necessaria ulteriore conversione. Se entrambi gli operandi (possibilmente promossi) hanno lo stesso segno, allora l'operando con il tipo più piccolo viene convertito nel tipo più grande.

Quando il segno differisce e il tipo dell'operando unsigned è lo stesso o più grande di quello dell'operando signed, l'operando signed viene convertito in unsigned. Ad esempio, dato un unsigned int e un int, l'int viene convertito in unsigned int. Vale la pena notare che se l'int ha un valore negativo, il risultato verrà convertito come descritto nella lezione sui tipi primitivi, con gli stessi risultati.

Il caso rimanente è quando l'operando signed ha un tipo più grande dell'operando unsigned. In questo caso, il risultato è dipendente dalla macchina. Se tutti i valori nel tipo unsigned stanno nel tipo più grande, allora l'operando unsigned viene convertito nel tipo signed. Se i valori non stanno, allora l'operando signed viene convertito nel tipo unsigned. Ad esempio, se gli operandi sono long e unsigned int, e int e long hanno la stessa dimensione, il long verrà convertito in unsigned int. Se il tipo long ha più bit, allora l'unsigned int verrà convertito in long.

Comprendere le Conversioni Aritmetiche

Un modo per comprendere le conversioni aritmetiche è studiare molti esempi:

bool      flag;       char                cval;
short     sval;       unsigned short      usval;
int       ival;       unsigned int        uival;
long      lval;       unsigned long       ulval;
float     fval;       double              dval;

3.14159L + 'a';     // 'a' promosso a int, poi quell'int convertito in long double
dval + ival;        // ival convertito in double
dval + fval;        // fval convertito in double
ival = dval;        // dval convertito (per troncamento) in int
flag = dval;        // se dval è 0, allora flag è false, altrimenti true
cval + fval;        // cval promosso a int, poi quell'int convertito in float
sval + cval;        // sval e cval promossi a int
cval + lval;        // cval convertito in long
ival + ulval;       // ival convertito in unsigned long
usval + ival;       // la promozione dipende dalla dimensione di unsigned short e int
uival + lval;       // la conversione dipende dalla dimensione di unsigned int e long

Nella prima addizione, la costante carattere minuscola 'a' ha tipo char, che è un valore numerico. Quale sia quel valore dipende dal set di caratteri della macchina. Supponendo di usare ASCII, 'a' ha il valore numerico 97. Quando aggiungiamo 'a' a un long double, il valore char viene promosso a int, e poi quel valore int viene convertito in long double. Il valore convertito viene aggiunto al letterale. Gli altri casi interessanti sono le ultime due espressioni che coinvolgono valori unsigned. Il tipo del risultato in queste espressioni è dipendente dalla macchina.

Esercizi

  • Date le definizioni di variabili in questa sezione, spiega quali conversioni avvengono nelle seguenti espressioni:

    if (fval) { /* ... */ }
    dval = fval + ival;
    dval + ival * cval;
    

    Soluzione:

    Nella prima espressione, fval viene convertito in bool. Se fval è 0.0, la conversione produce false; qualsiasi altro valore produce true.

    Nella seconda espressione, fval viene convertito in double così come ival viene convertito in double. La somma viene eseguita in double, e il risultato viene assegnato a dval.

    Nella terza espressione, ival viene convertito in int e cval viene promosso a int. La moltiplicazione viene eseguita in int, e il risultato viene convertito in double prima di essere sommato a dval, che è già un double.

  • Date le seguenti definizioni:

    char cval;
    int ival;
    unsigned int ui;
    float fval;
    double dval;
    

    identifica le conversioni di tipo implicite, se ce ne sono, che avvengono:

    cval = 'a' + 3;
    fval = ui - ival * 1.0;
    dval = ui * fval;
    cval = ival + fval + dval;
    

    Soluzione:

    • 'a' viene promosso a int, e 3 è già un int. La somma viene eseguita in int, e il risultato viene convertito in char per l'assegnazione a cval.
    • ival viene promosso a double e moltiplicato per 1.0. Allo stesso modo ui viene promosso a double e ad esso viene sottratto il valore calcolato prima. Il risultato viene poi convertito in float e poi assegnato a fval.
    • ui viene promosso a double e moltiplicato per fval. Il risultato viene poi assegnato a dval.
    • ival viene promosso a double, fval viene convertito in double, e la somma di questi due valori viene calcolata in double. Il risultato viene poi convertito in char per l'assegnazione a cval.

Altre Conversioni Implicite

Oltre alle conversioni aritmetiche, ci sono diversi tipi aggiuntivi di conversioni implicite. Questi includono:

  • Conversioni da Array a Puntatore: Nella maggior parte delle espressioni, quando usiamo un array, l'array viene automaticamente convertito in un puntatore al primo elemento in quell'array:

    int ia[10]; // array di dieci int
    int* ip = ia; // converte ia in un puntatore al primo elemento
    

    Questa conversione non viene eseguita quando un array viene usato con decltype o come operando dell'address-of (&), sizeof, o typeid (che vedremo nelle prossime lezioni). La conversione viene anche omessa quando inizializziamo un riferimento a un array. Come vedremo, una conversione di puntatore simile avviene quando usiamo un tipo funzione in un'espressione.

  • Conversioni di Puntatori: Ci sono diverse altre conversioni di puntatori: Un valore intero costante di 0 e il letterale nullptr possono essere convertiti in qualsiasi tipo puntatore; un puntatore a qualsiasi tipo non const può essere convertito in void *, e un puntatore a qualsiasi tipo può essere convertito in const void *. Vedremo nelle prossime lezioni che c'è una conversione di puntatore aggiuntiva che si applica ai tipi correlati per ereditarietà.

  • Conversioni a bool: C'è una conversione automatica dai tipi aritmetici o puntatore a bool. Se il valore del puntatore o aritmetico è zero, la conversione produce false; qualsiasi altro valore produce true:

    char *cp = get_string();
    if (cp) /* ... */ // true se il puntatore cp non è zero
    while (*cp) /* ... */ // true se *cp non è il carattere null
    
  • Conversione a const: Possiamo convertire un puntatore a un tipo non const in un puntatore al tipo const corrispondente, e similmente per i riferimenti. Cioè, se T è un tipo, possiamo convertire un puntatore o riferimento a T in un puntatore o riferimento a const T, rispettivamente:

    int i;
    const int &j = i; // converte un nonconst in un riferimento a const int
    const int *p = &i; // converte l'indirizzo di un nonconst nell'indirizzo di un const
    int &r = j, *q = p; // errore: conversione da const a nonconst non permessa
    

    La conversione inversa, ossia rimuovere un const di basso livello, non esiste.

  • Conversioni Definite da Tipi Classe: I tipi classe possono definire conversioni che il compilatore applicherà automaticamente. Il compilatore applicherà solo una conversione di tipo classe alla volta. Ritorneremo su questo argomento più avanti nelle prossime lezioni.

    I nostri programmi hanno già usato conversioni di tipo classe: Usiamo una conversione di tipo classe quando usiamo una stringa di caratteri in stile C dove è prevista una stringa di libreria e quando leggiamo da un istream in una condizione:

    // letterale stringa di caratteri convertito in tipo string
    string s, t = "un valore";
    
    // la condizione del while converte cin in bool
    while (cin >> s) { /* ... */ }
    

    La condizione (cin >> s) legge cin e produce cin come risultato. Le condizioni si aspettano un valore di tipo bool, ma questa condizione testa un valore di tipo istream. La libreria I/O definisce una conversione da istream a bool. Quella conversione viene usata (automaticamente) per convertire cin in bool. Il valore bool risultante dipende dallo stato dello stream. Se l'ultima lettura ha avuto successo, allora la conversione produce true. Se l'ultimo tentativo è fallito, allora la conversione a bool produce false.

Conversioni Esplicite

A volte vogliamo forzare esplicitamente la conversione di un oggetto in un tipo diverso. Ad esempio, potremmo voler usare la divisione in virgola mobile nel seguente codice:

int i, j;
double pendenza = i / j;

Per farlo, avremmo bisogno di un modo per convertire esplicitamente i e/o j in double. Per effettuare una conversione esplicita, possiamo usare un cast. Un cast è un'operazione che converte un'espressione in un tipo specificato. C++ supporta due forme di cast: il cast in stile C e i cast nominati.

Vediamo entrambe le forme.

Cast Nominati

Un cast nominato ha la seguente forma:

nome_cast<tipo>(espressione);

dove tipo è il tipo target della conversione, ed espressione è il valore da convertire. Se tipo è un riferimento, allora il risultato è un l-value. Il nome_cast può essere uno tra static_cast, dynamic_cast, const_cast e reinterpret_cast. Tratteremo dynamic_cast, che supporta l'identificazione del tipo a runtime, nelle prossime lezioni. Il nome_cast determina che tipo di conversione viene eseguita.

static_cast

Qualsiasi conversione di tipo ben definita, diversa da quelle che coinvolgono const di basso livello, può essere richiesta usando uno static_cast. Ad esempio, possiamo forzare la nostra espressione a usare la divisione in virgola mobile facendo un cast di uno degli operandi a double:

// cast usato per forzare la divisione in virgola mobile
double pendenza = static_cast<double>(j) / i;

Uno static_cast è spesso utile quando un tipo aritmetico più grande viene assegnato a un tipo più piccolo. Il cast informa sia il lettore del programma che il compilatore che siamo consapevoli e non siamo preoccupati della potenziale perdita di precisione. I compilatori spesso generano un avviso per assegnamenti di un tipo aritmetico più grande a un tipo più piccolo. Quando facciamo un cast esplicito, il messaggio di avviso viene disattivato.

Uno static_cast è anche utile per eseguire una conversione che il compilatore non genererà automaticamente. Ad esempio, possiamo usare un static_cast per recuperare un valore puntatore che era memorizzato in un puntatore void *:

// OK: l'indirizzo di qualsiasi oggetto non const
//     può essere memorizzato in un puntatore void *
void* p = &d;

// OK: converte void * indietro al tipo puntatore originale
double *dp = static_cast<double*>(p);

Quando memorizziamo un puntatore in un void * e poi usiamo uno static_cast per convertire il puntatore indietro al suo tipo originale, ci è garantito che il valore del puntatore sia preservato. Cioè, il risultato del cast sarà uguale al valore dell'indirizzo originale. Tuttavia, dobbiamo essere certi che il tipo a cui convertiamo il puntatore sia il tipo effettivo di quel puntatore; se i tipi non corrispondono, il risultato è indefinito.

const_cast

Un const_cast cambia solo un const di basso livello nel suo operando:

const char *pc;

// OK: ma scrivere attraverso p è indefinito
char *p = const_cast<char*>(pc);

Convenzionalmente diciamo che un cast che converte un oggetto const in un tipo non const "rimuove il const." Una volta che abbiamo rimosso il const di un oggetto, il compilatore non ci impedirà più di scrivere in quell'oggetto. Se l'oggetto non era originariamente un const, usare un cast per ottenere accesso in scrittura è legale. Tuttavia, usare un const_cast per scrivere in un oggetto const è indefinito.

Solo un const_cast può essere usato per cambiare la costanza di un'espressione. Tentare di cambiare se un'espressione è const con qualsiasi altra forma di cast nominato è un errore a tempo di compilazione. Similmente, non possiamo usare un const_cast per cambiare il tipo di un'espressione:

const char *cp;

// ERRORE: static_cast non può rimuovere const
char *q = static_cast<char*>(cp);

// OK: converte letterale stringa in string
static_cast<string>(cp);

// ERRORE: const_cast cambia solo la costanza
const_cast<string>(cp);

Un const_cast è più utile nel contesto di funzioni sovraccaricate, che descriveremo nelle prossime lezioni.

reinterpret_cast

Un reinterpret_cast generalmente esegue una reinterpretazione di basso livello del pattern di bit dei suoi operandi. Come esempio, dato il seguente cast

int *ip;
char *pc = reinterpret_cast<char*>(ip);

non dobbiamo mai dimenticare che l'oggetto effettivo indirizzato da pc è un int, non un carattere. Qualsiasi uso di pc che assume che sia un puntatore a carattere ordinario probabilmente fallirà a runtime. Ad esempio:

string str(pc);

probabilmente risulterà in un comportamento bizzarro a runtime.

L'uso di pc per inizializzare str è un buon esempio del perché reinterpret_cast è pericoloso. Il problema è che i tipi vengono cambiati, eppure non ci sono avvisi o errori dal compilatore. Quando abbiamo inizializzato pc con l'indirizzo di un int, non c'è errore o avviso dal compilatore perché abbiamo esplicitamente detto che la conversione era ok. Qualsiasi uso successivo di pc assumerà che il valore che contiene sia un char *. Il compilatore non ha modo di sapere che in realtà contiene un puntatore a un int. Quindi, l'inizializzazione di str con pc è assolutamente corretta, anche se in questo caso priva di significato o peggio! Rintracciare la causa di questo tipo di problema può rivelarsi estremamente difficile, specialmente se il cast di ip a pc avviene in un file separato da quello in cui pc viene usato per inizializzare una string.

Un reinterpret_cast è intrinsecamente dipendente dalla macchina. Usare reinterpret_cast in sicurezza richiede di comprendere completamente i tipi coinvolti così come i dettagli di come il compilatore implementa il cast.

Cast in Vecchio Stile

Nelle prime versioni di C++, un cast esplicito assumeva una delle seguenti due forme:

// notazione cast in stile funzione
tipo (expr);

// notazione cast in stile linguaggio C
(*tipo*) *expr*;

A seconda dei tipi coinvolti, un cast in vecchio stile ha lo stesso comportamento di un const_cast, uno static_cast o un reinterpret_cast. Quando usiamo un cast in vecchio stile dove uno static_cast o un const_cast sarebbe legale, il cast in vecchio stile fa la stessa conversione del rispettivo cast nominato. Se nessun cast è legale, allora un cast in vecchio stile esegue un reinterpret_cast. Ad esempio:

// ip è un puntatore a int
char *pc = (char *) ip;

ha lo stesso effetto di usare un reinterpret_cast.

I cast in vecchio stile sono meno visibili dei cast nominati. Poiché sono facilmente trascurati, è più difficile rintracciare un cast errato.

Consiglio

Evitare i Cast se Possibile

I cast interferiscono con il normale controllo dei tipi.

Di conseguenza, raccomandiamo fortemente che i programmatori evitino i cast. Questo consiglio è particolarmente applicabile ai reinterpret_cast. Tali cast sono sempre pericolosi.

Un const_cast può essere utile nel contesto di funzioni sovraccaricate, che tratteremo nelle prossime lezioni. Altri usi di const_cast spesso indicano un difetto di progettazione. Gli altri cast, static_cast e dynamic_cast, dovrebbero essere necessari raramente.

Ogni volta che scrivi un cast, dovresti pensare attentamente se puoi ottenere lo stesso risultato in un modo diverso. Se il cast è inevitabile, gli errori possono essere mitigati limitando l'ambito in cui il valore del cast viene usato e documentando tutte le assunzioni sui tipi coinvolti.

Esercizi

  • Assumendo che i sia un int e d sia un double, scrivi l'espressione i *= d in modo che esegua una moltiplicazione intera, piuttosto che in virgola mobile.

    Soluzione:

    i *= static_cast<int>(d);
    
  • Riscrivi ciascuno dei seguenti cast in vecchio stile per usare un cast nominato:

    int i;
    double d;
    const string *ps;
    char *pc;
    void *pv;
    
    pv = (void *)ps;
    i = int(*pc);
    pv = &d;
    pc = (char *) pv;
    

    Soluzione:

    pv = static_cast<void*>(const_cast<string*>(ps));
    i = static_cast<int>(*pc);
    pv = static_cast<void*>(&d);
    pc = static_cast<char*>(pv);
    
  • Spiega la seguente espressione:

    double pendenza = static_cast<double>(j/i);
    

    Soluzione:

    Questa espressione calcola la pendenza come il rapporto tra j e i. Tuttavia, poiché sia j che i sono di tipo int, l'operazione j/i esegue una divisione intera, che tronca qualsiasi parte decimale del risultato. Successivamente, il risultato intero viene convertito in double tramite static_cast<double>, ma a questo punto la precisione è già stata persa a causa della divisione intera. Per ottenere una divisione in virgola mobile, sarebbe più appropriato fare il cast di uno degli operandi prima della divisione, ad esempio: static_cast<double>(j) / i.