next up previous contents index
Next: Membri statici Up: Dettagli sulle classi. Templates Previous: Dettagli sulle classi. Templates   Indice   Indice analitico

Conversioni

Nella prima parte di questo testo abbiamo parlato in diverse occasioni delle conversioni in C++; si ricorda che esistono due tipi di conversioni che il compilatore può effettuare: conversioni implicite e esplicite. Le conversioni implicite avvengono in maniera del tutto automatica: è il compilatore ad occuparsene quando incontra una incongruenza di tipi; in tal caso cerca di convertire il dato che è in errore nel tipo atteso, qualora ciò non fosse possibile viene tornato un errore di compilazione. Le conversioni esplicite invece sono invocate dal programmatore tramite due sintassi, che abbiamo detto di considerare per il momento indifferenti: conversioni stile-C (o cast) e stile-C++. Vediamo un esempio

// 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);
effettua la chiamata al costruttore della classe 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;
Sebbene tale statement assomigli molto ad un assegnamento, in realtà si tratta di una inizializzazione della variabile 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);
il compilatore cerca un costruttore avente un solo argomento di tipo reale; non lo trova e quindi converte 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);
sarebbe questo il costruttore ad essere chiamato e non 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 $\frac{10}{7}$, 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).

ex-2
si programmi la classe 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;
ex-3
si definisca l'operatore di conversione a int e a double per la classe ArrayS; il valore intero tornato corrisponda al numero degli elementi dell'array, il valore reale corrisponda alla somma dei quadrati degli elementi dell'array


next up previous contents index
Next: Membri statici Up: Dettagli sulle classi. Templates Previous: Dettagli sulle classi. Templates   Indice   Indice analitico
Claudio Cicconetti
2000-09-06