Dr. Carlo Pescio
Costruttori: tutte le novita'

Pubblicato su C++ Informer No. 4, Aprile 1998
Immagino che alcuni di voi abbiano ormai scaricato da internet il draft standard C++, ed abbiano iniziato a studiarsi il documento. Ai ritardatari ricordo che possono reperire il draft partendo dalla mia home page, seguendo il link a "Siti Interessanti".
Leggere il draft, comunque, non e' un'impresa cosi' facile. Diventa ancora piu' complicato se il nostro obiettivo e' di capire "cosa e' cambiato" rispetto al linguaggio cui siamo bene o male abituati. Per questo ho deciso (gia' a partire dallo scorso numero di C++ Informer) di dedicare lo spazio ANSI/ISO ad una analisi delle novita' e delle sottili differenze introdotte con la standardizzazione del linguaggio, cercando quando possibile di spiegare in modo comprensibile anche le ragioni dietro i cambiamenti. Questa volta e' il turno dei costruttori, argomento sul quale sembra difficile aggiungere qualcosa di nuovo, ma...

Costruttori impliciti
Se non dichiariamo un costruttore di default o un costruttore di copia per le nostre classi, il compilatore ne generera' uno implicito, definito come inizializzazione di default member-wise nel primo caso, e come copia member-wise nel secondo caso.
Vi ricordo che un costruttore di default per una classe C e' un costruttore che puo' essere usato senza parametri (quindi: dichiarato senza parametri o con valore di default per ogni parametro), e che un costruttore di copia e' un costruttore che ha come tipo del primo parametro [const][volatile]C&, cui possono seguire altri parametri se hanno tutti un valore di default.
Iniziamo chiarendo un piccolo dubbio: i costruttori definiti implicitamente sono inline o meno, o (come sembrerebbe ragionevole) si tratta di un dettaglio implementation-defined? Stranamente, lo standard si pronuncia chiaramente su questo punto, ed afferma (paragrafi 12.1.5 e 12.8.5) che in entrambi i casi si tratta di funzioni inline e (ovviamente) pubbliche.
Piu' interessante e' pero' il prototipo introdotto dal compilatore, che varia a seconda delle situazioni. Data una classe C per cui non e' stato dichiarato un costruttore di copia, se vengono rispettate le seguenti condizioni:

1) ogni classe base immediata o virtuale B di C ha un costruttore di copia il cui primo parametro e' di tipo const [volatile] B&

2) tutti i data member non statici di C appartengono ad una classe (che chiamero' M), avente un costruttore di copia il cui primo parametro e' di tipo const [volatile] M&

Allora il costruttore di copia dichiarato implicitamente per C avra' prototipo C::C( const C& ). Se le due condizioni non vengono rispettate, avra' prototipo C::C( C& ).

Precisazioni sui costruttori di copia
Una modifica che puo' essere passata inosservata, rispetto alle regole dell'Annotated Reference Manual di Ellis/Stroustrup, e' che in una situazione come la seguente:

class Base 
  {
  } ;

class Derived : public Base 
  {
  public:
    Derived( const Base& x ) ;
  } ;

void f() 
  {
  Derived a ;
  Derived b( a ) ;  // (*)
  }
Alla linea marcata con (*) non viene chiamato il costruttore definito per Derived (anche se teoricamente sarebbe possibile farlo) ma viene invece chiamato il costruttore di copia definito implicitamente. Questo accade poiche', come dalla definizione precedente, un costruttore di copia per C deve avere come primo parametro C& (eventualmente cv-qualified), ed il costruttore di Derived non segue tale regola. Di conseguenza ne viene generato uno implicito, che poi ha un match migliore in (*) rispetto a quello definito esplicitamente.

Un altro punto delicato, sul quale noto costantemente una confusione nei programmatori, e' la "doppia scrittura" possibile nell'utilizzo di un costruttore di copia. Nell'esempio seguente:

C c1 ;
C c2( c1 ) ;
C c3 = c1 ;
Sia c2 che c3 vengono inizializzati dal costruttore di copia (piu' di un programmatore, a prima vista, afferma erroneamente che nel caso di c3 viene utilizzato l'operatore di assegnazione). La sintassi piu' diffusa e' sicuramente quella usata per c3, ma nel tempo mi sono convinto che la forma preferibile sia quella di c2, e che c3 venga adottata in gran parte a causa del "cattivo esempio" di libri, articoli e sorgenti.
Vi sono almeno tre buone ragioni per preferire la forma di c2 a quella di c3:

1) Evita l'errore fin troppo comune di pensare che venga tirato in ballo l'operatore di assegnazione.

2) E' l'unica forma permessa nella initialization list dei costruttori, sia per i data member che le classi derivate:
class C
  {
  public :
    C( D x ) : v( x ) {} 
  private :
    D v ;
  } ;
3) Essendo uniforme con l'uso degli altri costruttori (non di copia), evita casi patologici come il seguente:

class A 
  {
  public :
    A( int v ) ;
    A( const A& rhs ) ;
  private :
    int value;
  } ;

class B 
  {
  public :
    B( int v ) ;
    operator int() const ;
  private :
    int value;
  } ;

void f( const B& x )
  {
  A a1( x ) ;  // corretta
  A a2 = x ;   // errore!
  }
La dichiarazione di a1 e' corretta, ed a1 viene inizializzato con il costruttore A( int v ). Quello che avviene, ovviamente, e' che x viene convertito ad int e poi passato come parametro. Ma la dichiarazione di a2 e' sbagliata, perche' il costruttore usato si aspetta un parametro di tipo A&, ed x puo' essere convertito ad A solo usando due conversioni user-defined, il che non e' ammesso.
Conclusione: il mio consiglio e' di usare sempre la sintassi stile "chiamata di funzione" per i costruttori, evitando l'uso del segno di assegnazione per indicare qualcosa che non e' una assegnazione.

[static] default constructor
Si tratta di una modifica cosi' recente da non essere neppure riportata nel draft liberamente disponibile al pubblico. Come probabilmente saprete, nel codice seguente:

static int x ;

int main()
  {
  int i ;
  return( 0 ) ;
  }
la variabile x viene inizializzata a zero, mentre la variabile i non viene inizializzata affatto (valore indefinito). Tuttavia, se pensiamo ad oggetti di una classe C qualunque:

static C x ;

int main()
  {
  C i ;
  return( 0 ) ;
  }
il risultato e' che viene chiamato il costruttore di default in entrambi i casi. Questa disparita' e' quanto meno fastidiosa, anche se i programmatori C++ sono abituati alle disparita' di trattamento tra i tipi built-in e le classi. Tuttavia, con l'uso sempre piu' spinto dei template queste differenze cominciano a farsi sentire ancora di piu'. Il comitato di standardizzazione ha quindi cercato, per quanto possibile, di re-introdurre degli elementi uniformanti senza alterare il significato dei programmi preesistenti.
In particolare, la modifica introdotta in questo caso e' la seguente: un'espressione come int() ha come risultato lo stesso valore utilizzato per l'inizializzazione statica di un intero, ovvero zero. Fino ad ora, aveva valore indefinito. Ne consegue che l'espressione T() diventa un elemento uniformante tra tipi built-in e user defined. Vediamo le conseguenze:

static int x ;    // inizializzato a zero

int main()
  {
  int i ;         // valore indefinito
  int j = int() ; // inizializzato a zero

  return( 0 ) ;
  }
Come dicevo, questo diventa particolarmente importante quando scriviamo un template. Vediamo una funzione banale:

template< class T > T f()
  {
  T t ;
  return( t ) ;
  }
Se vogliamo garantire che t venga sempre inizializzata con il valore di default per gli oggetti di tipo T, dobbiamo scriverla come:

template< class T > T f()
  {
  T t = T() ;
  return( t ) ;
  }

Analogamente pensiamo ad una semplice classe:
template< class T > class C
  {
  public :
    C() {}
  private :
    T t ;
  } ;

Se implementiamo C come sopra, il data member t non verra' inizializzato per un oggetto di classe (ad es.) C< int >, mentre verra' inizializzato con il costruttore di default per C< A > dove A e' una classe qualunque. Tuttavia se riscriviamo C come segue:

template< class T > class C
  {
  public :
    C() : t( T() ) {}
  private :
    T t ;
  } ;
il data member t verra' sempre inizializzato. Lo stesso avviene se scriviamo C come segue:

template< class T > class C
  {
  public :
    C() : t() {}
  private :
    T t ;
  } ;
Ma attenzione: la sintassi t() non e' utilizzabile nelle dichiarazioni di variabile:

int t() ; // dichiara t come funzione che restituisce un int
Al momento, non mi risulta vi siano compilatori che seguono gia' (in modo documentato) le nuove regole sul valore di T() come espressione.

explicit
Come alcuni di voi ricorderanno, ho brevemente toccato questo tema nella Q&A del numero 2 di C++ Informer. Ogni costruttore con un solo parametro, o con i successivi parametri aventi valori di default, e' anche un "converting constructor", ovvero si comporta come un operatore di conversione e puo' essere usato implicitamente dal compilatore. L'esempio canonico dei problemi che questo puo' comportare e' proprio quello della succitata Q&A, da me utilizzato anche in "C++ Manuale di Stile":

class Array
  {
  public :
    Array( int dim ) ;
    // ...
  } ;

Array a( 5 ) ; // array di 5 interi
a = 6 ;        // assegna ad a un array di 6 interi
Il programmatore voleva scrivere a[ 0 ] = 6, ma ha commesso un errore che non viene segnalato e che porta a codice dal comportamento totalmente diverso.
In generale, dovremmo sempre stare attenti a definire un converting constructor. In teoria, gli unici converting constructor sensati sono quelli per cui vale una relazione IS-A tra la classe del costruttore ed il tipo del parametro. In tutti gli altri casi, stiamo aprendo la strada a possibili errori nascosti. Come rimediare, dunque, se il nostro costruttore ha bisogno di un solo parametro ma non vogliamo generare un converting constructor? Usando la keyword explicit, come segue:

class Array
  {
  public :
    explicit Array( int dim ) ;
    // ...
  } ;

Array a( 5 ) ; // array di 5 interi
a = 6 ;        // errore di compilazione
Un costruttore dichiarato explicit non verra' mai usato implicitamente dal compilatore, ma solo in fase di inizializzazione di un oggetto con la sintassi data sopra o al momento di un cast esplicito da parte del programmatore.

Template constructor
Con l'introduzione dei member template, anche i costruttori di una "normale" classe (non di un template) possono diventare dei template:

class C
  {
  template< class T > C( const T& t )
    {
    }
  } ;

C c1( 1 ) ;   // chiama C::C< const int& >
C c2( 'a' ) ; // chiama C::C< const char& >

Esistono pero' alcune particolarita' di cui dobbiamo essere a conoscenza:

1) Un template constructor non viene mai considerato un costruttore di copia (paragrafo 12.8.2.4). Ne consegue che se scriviamo:

C c3( c1 ) ;
viene chiamato il costruttore di copia generato implicitamente e non C::C< const C& >.

2) A differenza di ogni altro member template, non e' possibile usare la specifica esplicita dei parametri (paragrafo 14.8.1.4). Il tipo/valore di ogni parametro del template deve essere deducibile dai parametri del costruttore. Vediamo un esempio:

C c2< const int& >( 'a' ) ; // errore!
Questo punto, apparentemente minore, mi ha impedito di trovare una scappatoia ad un annoso problema del C++, che vi cito per completezza, anche se sarebbe un argomento da "No Limits".
Come noto, se dichiariamo un array di oggetti, ogni elemento viene inizializzato usando il costruttore di default. Pensiamo pero' ad una classe Point:

class Point
  {
  public :
    Point( int x, int y ) ;
    // ...
  } ;

Sarebbe comodo poter scrivere qualcosa del tipo:
Point v( -1, -1 )[ 100 ] ; // errore

per creare un vettore di 100 Point inizializzati a ( -1, -1 ). Naturalmente la sintassi data sopra e' errata, e dobbiamo rassegnarci a dichiarare l'array e ad inizializzarlo in seguito, il che peraltro ci impedisce di dichiarare v come const. Purtroppo il C++ ci costringe ad usare qualche trucco sporco e non rientrante, come l'uso di data member statici o variabili globali, per passare parametri al costruttore di default usato per inizializzare gli elementi dell'array.
Con l'introduzione dei member template, avevo sperato di poter scrivere qualcosa del genere:

class Point
  {
  public :
    template< int X = 0, int Y = 0 > 
    Point( int x = X, int y = Y ) ;
    // ...
  } ;

da chiamare come segue:
Point v1[ 100 ] ; // inizializza tutti a (0, 0)
Point< -1, -1 > v2[ 100 ] ; // inizializza tutti a (-1, -1)

Purtroppo questo non e' possibile. Una eventuale proposta in merito non sarebbe neppure passata, perche' in genere vengono richiesti almeno tre casi di utilizzo diversi. Se riuscite a pensare a qualche altra situazione in cui la rimozione del punto 14.8.1.4 si trasformi in nuove opportunita', potremmo sempre proporlo per il C++ 200x. In fondo le ragioni di base per il 14.8.1.4 sono piu' sintattiche che semantiche.

Errori e diagnostica del compilatore
Un costruttore per la classe C che ha un unico parametro di classe C e' errato, con diagnostica richiesta:

class C
  {
  C( C c ) ; // errore!
  } ;
Lo stesso avviene se seguono altri parametri ma hanno tutti un valore di default, o se c e' cv-qualified.
Una situazione molto pericolosa e' la seguente, per la quale non e' richiesta diagnostica:

class C
  {
  public :
    C( C& c, int x ) ;
  } ;


C :: C( C& c, int x = 0 )
  {
  // ...
  }

C c1 ;
C c2( c1 ) ;
Vediamo cosa succede: la dichiarazione della classe C, apparentemente, sembra priva di un costruttore di default. Ne viene quindi definito uno implicito. Poi segue la definizione del costruttore, che introduce un valore di default per x, "trasformando" il costruttore in uno di default. Il programma ha comportamento indefinito, e come dicevo non e' richiesta diagnostica.
L'unico modo per eliminare il problema (potete considerarla una ulteriore raccomandazione da aggiungere a quelle del mio "C++ Manuale di Stile") e' di evitare di introdurre i valori di default al momento della definizione, ma di farlo sempre e soltanto al momento della dichiarazione di ogni funzione.
Una nota importante: lo standard non dice nulla a proposito dell'analoga situazione con il costruttore di default (o se lo dice, io non sono riuscito a trovarlo). Ho ragione di ritenere (devo comunque accertare meglio) che valga lo stesso ragionamento, e che si debba assumere comportamento indefinito, nessuna diagnostica richiesta, anche in questo caso.

Un ulteriore esempio di codice con comportamento implementation-defined e' il seguente:

void* g = 0 ;
int y = 0 ;

class C
  {
  public :
    C()
      {
      if( g == 0 )
        y = 1 ;
      else
        y = 2 ;
      }
  } ;

int main()
  {
  g = new C() ;
  // che valore ha y??
  return( 0 ) ;
  }  
Lo standard non stabilisce l'ordine in cui avviene la sequenza [operator new, costruttore, assegnazione]. Sicuramente operator new viene chiamato per primo, ma costruttore ed assegnazione non hanno un ordine stabilito.

Conclusioni
Anche su un argomento "semplice" come i costruttori vi sono molti dettagli che possono facilmente sfuggire al programmatore, alcuni dei quali "sparpagliati" tra le note dello standard. Non tutti gli argomenti discussi sono gia' entrati a far parte dei compilatori in commercio: teneteli comunque presenti come informazioni di portabilita' verso il futuro.

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.