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.
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|