Conversioni di Tipo in C++
- 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 tipiconst
. - Le conversioni esplicite possono essere effettuate utilizzando cast nominati come
static_cast
,const_cast
, ereinterpret_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 inbool
. - 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 inbool
. Sefval
è 0.0, la conversione producefalse
; qualsiasi altro valore producetrue
.Nella seconda espressione,
fval
viene convertito indouble
così comeival
viene convertito indouble
. La somma viene eseguita indouble
, e il risultato viene assegnato adval
.Nella terza espressione,
ival
viene convertito inint
ecval
viene promosso aint
. La moltiplicazione viene eseguita inint
, e il risultato viene convertito indouble
prima di essere sommato adval
, che è già undouble
. -
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 aint
, e3
è già unint
. La somma viene eseguita inint
, e il risultato viene convertito inchar
per l'assegnazione acval
.ival
viene promosso adouble
e moltiplicato per1.0
. Allo stesso modoui
viene promosso adouble
e ad esso viene sottratto il valore calcolato prima. Il risultato viene poi convertito infloat
e poi assegnato afval
.ui
viene promosso adouble
e moltiplicato perfval
. Il risultato viene poi assegnato adval
.ival
viene promosso adouble
,fval
viene convertito indouble
, e la somma di questi due valori viene calcolata indouble
. Il risultato viene poi convertito inchar
per l'assegnazione acval
.
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
, otypeid
(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 nonconst
può essere convertito invoid *
, e un puntatore a qualsiasi tipo può essere convertito inconst 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 abool
. Se il valore del puntatore o aritmetico è zero, la conversione producefalse
; qualsiasi altro valore producetrue
: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 nonconst
in un puntatore al tipoconst
corrispondente, e similmente per i riferimenti. Cioè, seT
è un tipo, possiamo convertire un puntatore o riferimento aT
in un puntatore o riferimento aconst 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
) leggecin
e producecin
come risultato. Le condizioni si aspettano un valore di tipobool
, ma questa condizione testa un valore di tipoistream
. La libreria I/O definisce una conversione daistream
abool
. Quella conversione viene usata (automaticamente) per convertirecin
inbool
. Il valorebool
risultante dipende dallo stato dello stream. Se l'ultima lettura ha avuto successo, allora la conversione producetrue
. Se l'ultimo tentativo è fallito, allora la conversione abool
producefalse
.
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.
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 unint
ed
sia undouble
, scrivi l'espressionei *= 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
ei
. Tuttavia, poiché siaj
chei
sono di tipoint
, l'operazionej/i
esegue una divisione intera, che tronca qualsiasi parte decimale del risultato. Successivamente, il risultato intero viene convertito indouble
tramitestatic_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
.