// ex10_1_1.cpp void main() { int n = 1; double x = 1.5; // 1 // int* t = x; // errore: initialization to `int *' from `double' // 2 int a1 = x; // warning: initialization to `int' from `double' double y1 = n; // ok // 3 int a2 = int(x); // ok double y2 = double(n); // ok // 4 int* t = (int*)(int)(x); // ok: attenzione!!! }
Nel caso della conversione 1 il motivo per cui il compilatore torna un
messaggio di errore è evidente: si cerca di inizializzare un puntatore con
una variabile di tipo ``variabile reale'', mentre il tipo atteso è
``indirizzo di una variabile intera''; il compilatore non può in nessuna
maniera effettuare una conversione implicita. Nei casi 2 invece il
compilatore esegue due conversioni implicite: da int a
double senza dare messaggio di avvertimento, e da double a
int restituendo un warning, il quale ci segnala una possibile perdita
di dati. Infatti mentre l'intero 1
diventa semplicemente il reale
.0
, il reale 1.5
deve per forza di cose diventare 1
. Il
messaggio di warning scompare se effettuiamo una conversione esplicita (caso
3): siamo noi a segnalare le modalità di conversione, per cui il
compilatore suppone che non ci sia nessun errore o distrazione. Il caso
4 è il più insidioso di tutti: noi effettuiamo la stessa
conversione del caso 1 rendendola esplicita; il compilatore non si
lamenta poiché si tratta, appunto, di una conversione esplicita. Tuttavia
l'operazione è del tutto priva di significato: noi convertiamo il reale di
valore 1.5
nell'intero 1
, il quale viene poi convertito in
indirizzo di memoria; in sintesi il puntatore t
viene inizializzato con
la cella di memoria 0x1
, la quale non sappiamo assolutamente a cosa
corrisponde; se effettuassimo ad esempio delle operazioni di lettura o
(peggio) scrittura, potremmo creare seri danni al sistema.
Abbiamo detto che il reale 1.5
viene convertito nell'intero 1
;
chi ha stabilito questa regola ha pensato che fosse una conversione piuttosto
ragionevole. Ma chi stabilisce i meccanismi di conversioni per i nostri tipi,
cioè le classi? Ovviamente noi. Un primo metodo di conversione l'abbiamo
già implicitamente usato più volte; sappiamo che lo statement
Razionale q(1); |
Razionale
;
siccome viene passato un solo argomento, il secondo viene
automaticamente impostato a 1
sfruttando la tecnica degli
argomenti default nella chiamata di funzioni, che abbiamo detto essera
valida anche se la funzione è un costruttore. Ebbene, non abbiamo
forse convertito la variabile intera 1
nel tipo Razionale
?
Certo; è talmente evidente che lo statement precedente sia una vera e
propria conversione che esiste anche un sinonimo sintattico per esso:
Razionale q = 1; |
q
, con il
numero razionale implicitamente convertito a partire da 1
.
Ovviamente in un solo statement possono essere effettuate anche più
conversioni; nello statement
Razionale q(1.5); |
1.5
in 1
e poi tale
valore in un Razionale
. Ovviamente se avessimo definito un
costruttore con la seguente intestazione:
Razionale (double x, double y = 1); |
Razionale
(int = 0, int = 1)
. Vediamo il seguente esempio
// ex10_1_2.cpp #include <iostream.h> class A { public: A (int n = 0) { // costruttore default cout << "A::A(int = 0)\n"; } A (double x, double y = 0) { cout << "A::A(double, double = 0)\n"; } A (char c) { cout << "A::A(char)\n"; } }; void main() { A a, b = 1, c = 1.5, d = 'z', e = true; }
output:
A::A(int = 0)
A::A(int = 0)
A::A(double, double = 0)
A::A(char)
A::A(int = 0)
Per la classe A
noi forniamo un costruttore default (cioè che non
accetta argomenti) e tre costruttori che hanno per argomento, rispettivamente,
un intero, un reale ed un carattere; non abbiamo fatto altro che definire le
regole per la conversione da intero (o reale o carattere) nel tipo A
.
Il compilatore utilizzerà queste regole ogni qualvolta dovesse rendersi
necessario: inizializzazione di una variabile, passaggio di argomenti a
funzioni, ritorni di funzioni, valutazione di espressioni. Si noti che noi
non abbiamo specificato le regole di conversione dal tipo bool
a A
, tuttavia il compilatore non ritorna nessun messaggio di errore o
di avvertimento; infatti esiste nel linguaggio la regole di conversione
implicita da bool a int, la quale viene applicata
immediatamente, consentendo così di effettuare la conversione da intero a
A
. Naturalmente non sono ammesse ambiguità nelle conversioni
10.1, per
cui i seguente costruttori, ad esempio, non saranno accettati dal compilatore:
A (char = 'z'); A (double = 0); A (bool = true); A (); A (const int&); |
I primi quattro di essi creano ambiguità in caso di chiamata al costruttore priva di argomenti; l'ultima invece genera ambiguità nel caso si invochi il costruttore con un solo argomento, di tipo intero.
Abbiamo dunque sistemato la questione delle conversioni nel caso si voglia
convertire un tipo preesistente in uno creato da noi: basta definire uno o
più costruttori i quali abbiano come argomenti variabili dei tipi che si
intende convertire. Rimane tuttavia il problema inverso: se il compilatore
aspetta, ad esempio, un double vorremmo potergli passare un
Razionale
. Esiste un particolare operatore, detto di
conversione, il quale accetta prende il nome del tipo ``bersaglio'', ovvero
del tipo nel quale si intende convertire il nostro oggetto classe; tale
operatore non accetta argomenti.
// ex10_1_3.cpp #include "razionale.h" #include <iostream.h> class Razionale2 { Razionale q; public: Razionale2 (Razionale Q) { q = Q; } operator int() { return q.numeratore(); } operator double(); }; Razionale2::operator double() { return double(q.numeratore()) / double(q.denominatore()); } void main() { Razionale2 q = Razionale(3,7); int a = q; int b = 8 % q; cout << a << "\n" << b << "\n"; double x = q; double y = (7.0 / 3.0) * double(q); cout << x << "\n" << y << "\n"; }
output:
3
2
0.428571
1
Abbiamo creato la classe Razionale2
, la quale contiene come unico campo
dati un Razionale
; per la nuova classe abbiamo inoltre definito due
operatori di conversione: da Razionale2
a int e a
double. Come si vede, è possibile allora effettuare operazioni con
variabili di tipo Razionale2
facendo in modo che il compilatore
effettui in maniera implicita le conversioni nel giusto tipo, oppure
forzandole a piacere tramite conversioni esplicite.
Gli operatori di conversioni sono solitamente semplici da programmare; l'unica
difficoltà si pone in fase di progettazione della classe, non è infatti
sempre ovvio il meccanismo di conversione tra diversi tipi. Ad esempio nella
classe Razionale2
abbiamo imposto che un numero razionale convertito in
un numero reale debba semplicemente corrispondere al rapporto tra numeratore e
denominatore; si tratta di una scelta praticamente obbligata, che non offre
spazi a fraintendimenti o ad abusi delle conversioni. Non è affatto ovvio
invece convertire un numero razionale in un intero; noi, come esempio, abbiamo
imposto che dovesse essere restituito il numeratore: si tratta di una scelta
del tutto arbitraria, la quale non è basata su effettive proprietà
matematiche o su convenzioni universali, per cui è facile che un
utilizzatore della nostra classe sia tratto in errore dalla scarsa
intuitività di tale operatore. Ad esempio:
// ex10_1_4.cpp #include "razionale.h" #include <iostream.h> class Razionale3 { Razionale q; public: Razionale3 (Razionale Q) { q = Q; } operator int() { return q.numeratore(); } }; void main() { Razionale3 q = Razionale(3,7); cout << q+1 << "\n"; }
è del tutto lecito aspettarsi che venga stampato un qualcosa corrispondente
a , mentre il risultato dell'operazione
q+1
è un intero
di valore 4
; infatti q
viene prima di tutto convertito in un
intero corrispondente al suo numeratore (3
), poi a esso viene sommato
1
. Il risultato è un probabile errore di utilizzo. In casi simili ci
sono due possibilità: non definire nessun operatore di conversione,
per cui in casi incerti il compilatore torna un messaggio di errore; stabilire
le regole di conversione ma ben specificarle nella documentazione relativa
alla nostra classe (per classi semplici basta inserire un commento ben
visibile vicino alla definizione dell'operatore di conversione).
Complesso2
, la quale contenga come unico
campo dati un numero complesso, ivi definendo l'operatore di conversione a
double, il quale ritorni il modulo del numero complesso;
ArrayS
; il valore intero tornato
corrisponda al numero degli elementi dell'array, il valore reale corrisponda
alla somma dei quadrati degli elementi dell'array