Cerca nel sito:
ricerca
avanzata

Frasi Celebri...

Una Lettera d'amore non arrossisce.

Anonimo 

Sondaggio:

In quale citt? vi piacerebbe vivere?

New York
Londra
Roma
Madrid
Parigi
Milano
Tokyo
Berlino
Altro

visualizza risultati


 

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.

 

successivo
–«  INDICE  »–

 

 

 

 
Powered by paper&pencil (carta&matita ) - Copyright © 2001-2019 Cataldo Sasso