Appendice A - Conversioni di tipo
Per conversione di tipo si intende una operazione volta a trasformare un valore
di un certo tipo in un altro valore di altro tipo. Questa operazione e` molto
comune in tutti i linguaggi, anche se spesso il programmatore non se ne rende
conto; si pensi ad esempio ad una operazione aritmetica (somma, divisione...)
applicata ad un operando di tipo int e uno di tipo float .
Le operazioni aritmetiche sono generalmente definite su operandi dello stesso
tipo e pertanto non e` possibile eseguire immediatamente l'operazione; si rende
quindi necessario trasformare gli operandi in modo che assumano un tipo comune
su cui e` possibile operare. Quello che generalmente fa, nel nostro caso, un
compilatore di un qualsiasi linguaggio e convertire il valore intero in un reale
e poi eseguire la somma tra reali, restituendo un reale.
Non sempre comunque le conversioni di tipo sono decise dal compilatore, in alcuni
linguaggi (C, C++, Turbo Pascal) le conversioni di tipo possono essere richieste
anche dal programmatore, distinguendo quindi tra conversioni implicite e conversioni
esplicite. Le prime (dette anche coercizioni) sono eseguite dal
compilatore in modo del tutto trasparente al programmatore (come nel caso esposto
sopra), mentre le seconde sono quelle richieste esplicitamente con una opportuna
sintassi.
int i = 5;
float f = 0.0;
double d = 1.0;
d = f + i;
d = (double)f + (double)i;
// questa riga si legge: d = ((double)f) + ((double)i)
L'esempio precedente mostra entrambi i casi.
Nel primo assegnamento, l'operazione di somma e` applicata ad un operando intero
e uno di tipo float , per poter eseguire la somma il compilatore
C++ prima converte i al tipo float , quindi esegue
la somma (entrambi gli operandi hanno lo stesso tipo) e poi, poiche` la variabile
d e` di tipo double , converte il risultato al tipo
double e lo assegna alla variabile.
Nel secondo assegnamento, il programmatore richiede esplicitamente la conversione
di entrambi gli operandi al tipo double prima di effettuare la
somma e l'assegnamento (la conversione ha priorita` maggiore delle operazioni
aritmetiche).
Una conversione di tipo esplicita puo` essere richiesta con la sintassi
( < NuovoTipo > ) < Valore >
oppure
< NuovoTipo > ( < Valore > )
ma quest'ultimo metodo puo` essere utilizzato solo con nomi semplici (ad esempio
non funziona con char * ).
NuovoTipo puo` essere una qualsiasi espressione di tipo, anche una
che coinvolga tipi definiti dall'utente; ad esempio:
int a = 5;
float f = 2.2;
(float) a
// oppure...
float (a)
// se Persona e` un tipo definito dal programmatore...
(Persona) f
// oppure...
Persona (f)
Le conversioni tra tipi primitivi sono gia` predefinite nel linguaggio e possono
essere esplicitamente utilizzate in qualsiasi momento, il compilatore comunque
le utilizza implicitamente solo se il tipo di destinazione e` compatibile con
quello di origine (cioe` puo` rappresentare il valore originale).
Un fattore da tener presente, quando si parla di conversioni, e` che non sempre
una conversione di tipo preserva il valore: ad esempio nella conversione da
float a int in generale si riscontra una perdita di
precisione, (in effetti in una conversione float a int
il compilatore non fa altro che scartare la parte frazionaria, se il valore
non e` rappresentabile il risultato e` indefinito).
Da questo punto di vista si puo` distinguere tra conversione di tipo con perdita
di informazione e conversione di tipo senza perdita di informazione. Tra le
conversioni senza perdita di informazioni (safe) troviamo le conversioni
triviali:
DA: |
A: |
T |
T& |
T& |
T |
T[ ] |
T* |
T(args) |
T (*) (args) |
T |
const T |
T |
volatile T |
T* |
const T* |
T* |
volatile T* |
Altre conversioni considerate safe sono:
Le conversioni riportate nella figura precedente insieme a quelle triviali
sono le uniche ad essere garantite safe, alcune implementazioni potrebbero comunque
fornire altre conversioni safe ma per esse non ci sarebbero garanzie di portabilita`.
Le conversioni da e verso un tipo definito dal programmatore richiedono che
il compilatore sia informato riguardo a come eseguire l'operazione.
Per convertire un tipo primitivo (float , int , unsigned
int...) in un nuovo tipo e` necessario che questo nuovo tipo sia una classe
(o una struttura) e che sia definito un costruttore che ha come unico argomento
un parametro del tipo primitivo:
class Test {
public:
Test(int a);
private:
float member;
};
Test::Test(int a) {
member = (float) a;
}
Il metodo va naturalmente bene anche quando il tipo di partenza e` anch'esso
un tipo definito dal programmatore.
Per convertire invece un tipo utente ad un tipo primitivo e` necessario definire
un operatore di conversione. Con riferimento al precedente esempio, il metodo
da seguire e` il seguente:
class Test {
public:
Test(int a);
operator int();
private:
float member;
};
Test::operator int() { return (int) member; }
Se cioe` si desidera poter convertire un tipo utente X in un tipo
primitivo (o anche un altro tipo utente) T bisogna definire un
operatore con nome T :
X::operator T() { /* codice operatore */ }
Si noti che non e` necessario indicare il tipo del valore restituito, e` implicito
nel nome dell'operatore stesso.
C'e` un aspetto che bisogna sempre tener presente: quando si definisce un operatore
di conversione, questo non necessariamente e` disponibile solo al programmatore,
ma lo puo` essere anche al compilatore (se viene dichiarato nella sezione public
della classe )che potrebbe quindi utilizzarlo senza dare alcun avviso.
Nel caso dei costruttori pubblici il linguaggio fornisce un meccanismo di controllo
per impedirne un uso automatico del compilatore:
class Test {
public:
explicit Test(int a);
Test(char c);
private:
float member;
};
Test::Test(int a): member((float) a) {}
Test::Test(char c): member((float) c) {}
int main(int, char* []) {
Test A(5); // Ok!
Test B('c'); // Ok!
A = 7; // Errore cast implicito non possibile!
A = Test(7); // Ok, cast esplicito!
A = 'b'; // Ok, cast implicito possibile!
return 0;
}
La keyword explicit purtroppo e` applicabile solo ai costruttori,
non e possibile applicarla agli operatori di conversione; come conseguenza di
cio` per impedire al compilatore l'uso automatico di un operatore di conversione
e` necessario renderlo privato o protetto e definire una funzione di forwarding
(se siamo interessati a rendere fruibile l'operazione dall'esterno della classe):
class Test {
public:
explicit Test(int a);
Test(char c);
int ToInt();
private:
operator int();
float member;
};
int Test::ToInt() {
return int();
}
Riassumendo e` possibile definire in diversi modi una operazione di conversione,
in alcuni casi possiamo scegliere tra utilizzare un costruttore, oppure definire
un operatore di conversione; in altri casi non abbiamo scelte (tipicamente per
i cast verso un tipo primitivo).
La notazione che abbiamo visto sopra per richiedere esplicitamente un cast e`
derivata direttamente dal C e soffre di alcuni problemi:
- Alcuni cast tipici del C++ sono soggetti a potenziali fallimenti (si pensi
ad un cast da classe base a classe derivata) e deve essere possibile gestire
tale eventualita`;
- I cast sono una violazione del type system, si tratta di operazioni rischiose
e solitamente non portabili. La vecchia sintassi non consente una veloce individuazione
indispensabile nella manutenzione del software.
Il C++ introduce di conseguenza una nuova sintassi:
const_cast < T > (Expr)
static_cast < T > (Expr)
reinterpret_cast < T > (Expr)
dynamic_cast < T* > (Ptr)
Nella prima forma (const_cast ), Expr deve essere di
tipo T eccetto che per l'uso dei modificatori const
e/o volatile , tale sintassi serve solo a rimuovere (aggiungere) tali
modificatori da (a) Expr in qualunque combinazione.
static_cast e` utilizzato per risolvere un qualunque cast (eccetto
quelli risolti da const_cast ), usate questa sintassi quando siete
sicuri che l'operazione e` correttamente fattibile.
reinterpret_cast e` in assoluto il tipo di cast piu` pericoloso perche`
esegue una semplice reinterpretazione dell'argomento che viene visto come una
sequenza di bit da mappare sulla base di T .
Infine dynamic_cast si usa prevalentemente per eseguire operazioni
di downcast (conversione verso classi derivate) quando e` possibile il fallimento
(in caso contrario potrebbe essere utilizzato static_cast ). Si noti
che l'argomento (Ptr ) deve essere un puntatore o un riferimento e
che dynamic_cast restituisce un puntatore (vedi sintassi) o in alternativa
un riferimento. L'operazione di downcast puo` essere eseguita solo se la classe
base e` polimorfica (ha cioe` metodi virtuali), questa operazione richiede il
RTTI ed e` eseguita a run time. In caso di fallimento di un downcast, viene sollevata
una eccezione (bad_cast ) per i cast a riferimento, altrimenti (conversione
verso puntatore) viene restituito il puntatore nullo.
dynamic_cast puo` comunque essere utilizzato anche per eseguire upcast
(cast verso classe base), in tal caso l'operazione viene risolta a compile time.
Eccone alcuni esempi d'uso della nuova sintassi:
// Downcast (risolto a run time):
Persona* Caio = new Studente(/*...*/;
Studente* Pippo = dynamic_cast < Studente* > (Caio);
// rimozione di const:
const long ConstObj = 10;
long* LongPtr = const_cast < long* > ( & ConstObj );
// cast bruto:
int* Ptr = new int(7);
double* DPtr = reinterpret_cast < double* > (Ptr);
// cast risolto a compile time:
Caio = static_cast < Persona* > (Pippo);
L'operazione di downcast (il primo cast dell'esempio) viene risolta a run time,
il compilatore genera codice per verificare la fattibilita` dell'operazione
e se fattibile procede alla conversione (chiamando l'apposito operatore), altrimenti
verrebbe restituito il puntatore nullo.
Il secondo esempio mostra come eliminare il const : viene calcolato
l'indirizzo dell'oggetto costante (tipo const long* ) e quest'ultimo
viene poi convertito in long* .
Il terzo esempio mostra invece un tipico cast in cui semplicemente si vuole
interpretare una sequenza di bit secondo un nuovo significato, nel caso in esame
un int* viene interpretato come se fosse un double* .
Questo genere di conversione e` tipicamente dipendente dall'implementazione
adottata.
Infine l'ultimo esempio mostra come risolvere a run time un cast verso classe
base a partire da una classe derivata (operazione che sappiamo essere sicura).
Si noti che quella vista e` solo una sintassi, l'operazione di cast effettiva
viene svolta richiamando gli appositi operatori che devono quindi essere definiti;
ad esempio:
Studente Sempronio(/* ... */
Persona Ciccio = static_cast < Persona > (Sempronio);
int Integer = 5;
double Real = static_cast < double > (Integer);
Integer = static_cast < Persona > (Ciccio);
I primi due cast possono essere risolti perche` nel primo caso Studente
e` un sottotipo di Persona e l'operatore di conversione e` implicitamente
definito; nel secondo caso l'operatore invece e` gia` definito dal linguaggio.
L'ultimo esempio invece genera un errore se la classe Persona non
definisce un operatore di conversione a int .
|