Dr. Carlo Pescio
I nuovi cast

Pubblicato su C++ Informer No. 6, Gennaio 1999

Uno dei problemi maggiori del buon vecchio C e' la facilita' con cui il programmatore puo' sovvertire il gia' debole type system: con un semplice cast, dall'innocente sintassi "(T)expr", possiamo convertire il tipo dell'espressione expr a T. Inizialmente, il C++ ha ereditato questa caratteristica dal C, con risultati in un certo senso anche peggiori, poiche' il cast "C-stile" si e' addirittura arricchito di nuovi significati. In effetti, in C++ il cast "(T)expr" puo' avere i seguenti significati:
* una reinterpretazione dei bit del valore di expr, come nella conversione puntatore - intero.
* una conversione di tipo aritmetico, ad esempio da int a float.
* operazioni aritmetiche sui puntatori, ad esempio per convertire un puntatore ad una classe derivata da due classi base in un puntatore ad una delle classi base. Questa possibilita' e' presente solo in C++.
* la modifica di attributi come const o volatile.
* un risultato dipendente dall'implementazione, come il cast tra reference a classi indipendenti.
Il problema maggiore, se il nostro scopo e' la chiarezza del codice, e' che l'azione che stiamo compiendo non e' evidente dal codice stesso; peggio ancora, se cambiano alcune premesse cambia anche l'azione compiuta, senza che il codice debba cambiare o che venga generato un messaggio di errore dal compilatore. Consideriamo il listato seguente:

class DoubleDerived: public FirstBase, public SecondBase
  {
  // ...
  } ;

void f( DoubleDerived* dd )
  {
  ((SecondBase*)dd)->SomeFunction() ;
  }
Nella funzione f(), il programmatore vuole chiamare la funzione SomeFunction() dell'oggetto puntato da dd, garantendosi che venga chiamata la funzione implementata nella classe SecondBase. Il codice di per se' e' lecito, tuttavia se in seguito cambiamo la definizione di DoubleDerived non ereditando piu' da SecondBase, il codice diventa logicamente errato ma continua a compilare senza il minimo messaggio di errore dal compilatore, e con risultati indefiniti a run-time. Notiamo che il costrutto dell'esempio precedente, relativamente comune nella pratica della programmazione, poteva essere implementato senza usare il cast:

class DoubleDerived: public FirstBase, public SecondBase
  {
  // ...
  } ;

void f( DoubleDerived* dd )
  {
  dd->SecondBase::SomeFunction() ;
  }
In tal modo, se DoubleDerived non fosse derivata da SecondBase, il compilatore genererebbe un opportuno messaggio di errore.
Per quanto sia raccomandabile evitare ogni cast non indispensabile, in un programma reale non e' comunque possibile eliminarli totalmente. Il comitato ANSI/ISO ha tuttavia approvato alcuni nuovi operatori di cast, che sono gia' implementati da quasi tutti i compilatori commerciali, e che rimediano a molte mancanze del cast originale del C:
* esistono operatori diversi per i diversi significati che il cast puo' assumere.
* hanno una sintassi che li rende facilmente riconoscibili e facili da trovare (e/o contare) con tool di ricerca.
* viene eseguito un controllo statico a compile-time quando e' sufficiente, o un controllo a run-time quando necessario, sfruttando il nuovo supporto per il Run-Time Type Identification.
* sono in grado di operare correttamente anche nei casi in cui il cast C-style normalmente fallisce (ovvero genera un risultato errato).
In quanto segue vedremo i vari operatori e le relative circostanze di utilizzo.

L'operatore static_cast

Uno degli utilizzi piu' comuni del cast in C++ e' il cosidetto downcast: dopo un cast implicito da puntatore a classe derivata a puntatore a classe base, si desidera eseguire l'opposto: convertire il puntatore da classe base all'originale classe derivata. In opportuni contesti, cio' e' corretto e puo' essere realizzato anche senza informazioni sui tipi a run-time, ovvero la conversione puo' essere risolta staticamente a compile-time.
Uno dei casi piu' noti di conversione base-derivata che non viola il type system e' rappresentato dalle liste (o stack o altro contenitore) di oggetti omogenei, realizzate senza template. In tal caso, la lista "generica" memorizza puntatori ad oggetti di classe base, ed una semplice classe derivata si occupa di inserire (con cast implicito) e recuperare (con cast esplicito) puntatori ad oggetti di classe derivata. Tralasciando per il momento l'opportunita' o meno di una simile soluzione, in questo contesto potremmo opportunamente utilizzare l'operatore static_cast, come nel listato seguente:

class Base
  {
  // ...
  } ;

class Derived : public Base
  {
  // ...
  } ;

class StackOfBase
  {
  public :
    void Push( Base* item ) ;
    Base* Top() ;
    // ...
  } ;

class StackOfDerived : private StackOfBase
  {
  public :
    void Push( Derived* item ) 
      { 
      StackOfBase :: Push( item ) ; 
      }
    Derived* Top() 
      { 
      return( static_cast< Derived* >( StackOfBase :: Top() ) ) ;
      }
    // ...
  } ;
    
In generale, ogni volta che esiste una convesione implicita dal tipo A al tipo B, e' possibile richiedere sia il cast statico di A in B (rendendo quindi esplicito un cast implicito), sia il cast statico di B in A (con l'eccezione che il cast statico non puo' modificare l'attributo const), invertendo quindi una conversione esplicita. Ovviamente, nel caso precedente, se il puntatore a Base non punta effettivamente ad un oggetto di classe Derived, il risultato e' indefinito, poiche' static_cast non esegue alcun controllo a run-time.
Lo static_cast e' quindi un cast piu' limitato del cast C-style, che trova la sua collocazione ideale in situazione di downcast controllato, dove tipicamente chi fa il downcast e' anche responsabile del precedente upcast.

L'operatore const_cast
La conversione da oggetto const a oggetto non-const e' intrinsecamente pericolosa e raramente necessaria (per ulteriori considerazioni, vedere [1] al capitolo 7); per tale ragione, e' stato deciso che l'operatore static_cast debba conservare l'attributo const. Codice come quello del listato seguente dovrebbe quindi generare un errore di compilazione su ogni compilatore allineato allo standard:

class C
  {
  // ...
  } ;

int main()
  {
  const C constObject ;
  C* nonConstPtr = static_cast< C* >( &constObject ) ; // Errore
  return( 0 ) ;
  }
Dovendo necessariamente convertire un oggetto const in uno non-const, potete comunque utilizzare l'operatore const_cast, come nel listato seguente:

class C
  {
  // ...
  } ;

int main()
  {
  const C constObject ;
  C* nonConstPtr = const_cast< C* >( &constObject ) ;
  return( 0 ) ;
  }
Il const_cast puo' sia mettere che togliere gli attributi const e volatile, in qualunque combinazione. Dovrebbe trovare un uso piuttosto limitato, tipicamente dovuto a codice vecchio o ad interfacce mal progettate.

L'operatore dynamic_cast
L'operatore static_cast, cosi' come il cast C-style, si basa esclusivamente su informazioni disponibili a compile-time; in tal senso, la conversione puo' portare a risultati inattendibili, ad esempio nel caso di ereditarieta' multipla:

class Base
  {
  // ...
  } ;

class Derived : public Base
  {
  // ...
  } ;

void f( Base* p )
  {
  Derived* dp = static_cast< Derived* >( p ) ;
  // ... usa dp ...
  }

class DoubleDerived : public Derived
  {
  // ...
  } ;

class OtherDerived : public Base
  {
  // ...
  } ;

class Multiple : public DoubleDerived, public OtherDerived
  {
  // ...
  } ;

int main()
  {
  OtherDerived* dd = new Multiple ; // cast implicito
  f( dd ) ;  // cast implicito
  return( 0 ) ;
  } ;
L'esempio e' abbastanza complesso e richiede una spiegazione dettagliata: all'interno di main() creiamo un oggetto di classe Multiple, e richiediamo un cast implicito ad una delle sue classi base (OtherDerived); chiamiamo poi f(), richiedendo un ulteriore cast implicito a Base. Osserviamo che l'oggetto di classe Base che e' stato passato ad f() non e' un sotto-oggetto di Derived: e' stato ottenuto dal cast di OtherDerived* a Base*, ovvero seguendo un percorso diverso nella gerarchia di classi. L'operatore static_cast non potra' pertanto dare un risultato corretto: semplicemente non dispone di sufficienti informazioni per convertire un puntatore ad una classe "sorella". Ovviamente, lo stesso accadrebbe utilizzando il cast C-style.
L'unica soluzione percorribile e' di avere a disposizione a run-time sufficienti informazioni da navigare la gerarchia di classi e modificare opportunamente il puntatore. A tal fine e' stato introdotto l'operatore dynamic_cast: in una espressione dynamic_cast<T>(v), T deve essere un tipo puntatore o reference ad una classe gia' definita, oppure void*. Se T e' un tipo puntatore a classe, e v e' un puntatore ad una classe di cui T e' una classe base accessibile, allora il risultato e' un puntatore all'unico sotto-oggetto di classe T. Analogamente nel caso dei reference. In tutti gli altri casi, ad esempio nel caso di classi "sorelle", v deve puntare ad un tipo polimorfo, ovvero avente almeno una funzione virtuale (l'esigenza nasce da ragioni implementative: le informazioni run-time sul tipo vengono mantenute in una struttura associata alla virtual-function table dell'oggetto; solo le classi con almeno una funzione virtuale dispongono di una vtable, quindi sono le uniche a poter beneficiare del controllo a run-time sui tipi). In questo caso, viene eseguito un controllo a run-time per verificare la convertibilita': in caso di fallimento, se si tratta di un cast di puntatori verra' restituito 0, se si tratta di un cast di reference verra' generata un'eccezione di tipo bad_cast.
Notiamo che dynamic_cast puo' anche essere usato per convertire un puntatore ad una virtual base class in un puntatore ad una classe derivata, operazione impossibile con il cast C-style. Infine, una applicazione interessante e' il dynamic_cast a void* di un puntatore ad un oggetto: in questo caso, il puntatore ottenuto punta all'oggetto completo di cui il parametro faceva parte:

class A
  {
  public :
   virtual ~A() {}
  } ;

class B
  {
  public :
   virtual ~B() {}
  } ;

class C : public A, public B
  {
  } ;

void f()
  {
  C c ;
  B* p = &c ; // punta al sotto-oggetto B
  void* q = dynamic_cast< void* >( p ) ;
  // q punta all'intero oggetto c
  }
L'uso di questa feature, tuttavia, e' probabilmente piu' adatto alla rubrica No Limits (in effetti, lo usero' in questa stessa puntata come parte del codice per cambiare dinamicamente la classe di un oggetto).
Ovviamente dynamic_cast ha un overhead non nullo a run-time (esercizio: in quale occasione anche uno static_cast ha un overhead non nullo a run-time?), dipendente ovviamente dal compilatore. Attualmente dynamic_cast e' implementato piuttosto male sui vari compilatori commerciali, e potete pensare che si rubi diverse centinaia di cicli di clock. Per contro, l'uso dovrebbe essere piuttosto limitato: esclusi i casi di multiple dispatch simulato, l'uso del dynamic_cast e' spesso una conseguenza di un uso limitato di funzioni virtuali, o di una gerarchia di classi mal progettata.

L'operatore reinterpret_cast
Alcuni cast espliciti C-style vengono eseguiti tra puntatori a tipi base, ad esempio da char* ad int*, oppure tra puntatori/reference a classi scorrelate, ovvero non appartenenti ad una componente connessa nel grafo di ereditarieta'. Tali conversioni sono inerentemente pericolose e dipendenti dall'implementazione, in quanto il valore di un puntatore a char potrebbe non essere valido come valore di puntatore a int (che in un particolare sistema potrebbe, ad esempio, dover assumere solo valori multipli di 32). In questi casi, l'apparente innocenza di un cast C-style puo' seriamente fuorviare il programmatore nella comprensione del codice: e' invece opportuno usare il nuovo operatore reinterpret_cast, come nel listato seguente:

int main()
  {
  char* charBuffer = new char[ 100 ] ;
  int* intBuffer = reinterpret_cast< int* >( charBuffer ) ;
  // usa intBuffer per accedere a coppie di caratteri, 
  // assume che un intero sia grande quanto due caratteri. 
  // Dipendente dall'implementazione!
  return( 0 ) ;
  }
Il risultato di reinterpret_cast e', secondo lo standard, implementation defined. Questo significa che ogni compilatore puo' dare una propria implementazione, che deve poi seguire in modo coerente (a differenza di "undefined behaviour", che significa che ogni comportamento e' accettabile per il codice generato). Di norma, quello che avviene e' una brutale reinterpretazione degli stessi bit che compongono l'argomento: ad esempio, potremmo convertire un puntatore in un valore di tipo integral, manipolarne i bit, e riconvertirlo in puntatore sempre con reinterpret_cast. Tuttavia, ricordate che ogni impiego non banale di reinterpret_cast ha risultato dipendente dall'implementazione.

Cosa manca? Fondamentalmente mancano solo due ulteriori tipi di cast. Il primo e' un implicit_cast, che serve a rendere esplicita una conversion implicita. E' un argomento un po' da No Limits, di cui ho gia' parlato al Developer's Forum nell'intervento "Template Magic". Chi e' interessato ad una discussione su C++ Informer, si faccia sentire via email. Il secondo e' un checked_cast, che in versione debug faccia anche il confronto tra dynamic e static cast, e che in ogni caso restituisca lo static_cast. Questo e' cosi' facile da essere lasciato come utile (nel senso che il risultato ha una reale utilita') esercizio.

Conclusioni
La discussione precedente e' un po' semplificata rispetto alle minuzie dello standard, ma cattura gran parte delle informazioni utili. Sicuramente utilizzare i nuovi cast richiede un codice piu' "verboso", ma devo dire che dopo alcuni anni di pratica, considero i nuovi cast decisamente migliori dei precedenti. Piu' di una volta ho distrattamente scritto static_cast (pensando semplicemente ad una risoluzione a compile-time), solo per vedermi ricordare dal compilatore che static_cast era inadatto alla situazione, che magari richiedeva un reinterpret_cast. In questi casi, chiarire meglio cosa stiamo chiedendo al compilatore e' anche il modo migliore per chiarire cosa stiamo facendo a chi legge il nostro codice.

Bibliografia
[1] Carlo Pescio, "C++ Manuale di Stile", Edizioni Infomedia, 1995.

Biografia
Carlo Pescio (pescio@eptacom.net) svolge attività di consulenza, progettazione e formazione in ambito internazionale. Ha svolto la funzione di Software Architect in grandi progetti per importanti aziende europee e statunitensi. È autore di numerosi articoli su temi di ingegneria del software, programmazione C++ e tecnologia ad oggetti, apparsi sui principali periodici statunitensi, nonché dei testi "C++ Manuale di Stile" ed "UML Manuale di Stile". Laureato in Scienze dell'Informazione, è membro dell'ACM, dell'IEEE e dell'IEEE Technical Council on Software Engineering.