Dr. Carlo Pescio
Funzioni virtuali nei costruttori

Pubblicato su C++ Informer No. 8, Giugno/Luglio 1999

Una delle "sorprese" inevitabili nell'apprendimento del C++ e' la scoperta del comportamento delle funzioni virtuali all'interno dei costruttori. Quasi ogni programmatore C++ si ritrova prima o poi a scrivere codice del genere:

class B
  {
  public :
    B() { Init() ; }
  protected :
    virtual void Init() { std::cout << "B::B()" ; }
  } ;

class D : public B
  {
  public :
    D() : B() {}
  protected :
    virtual void Init() { std::cout << "D::D()" ; }
  } ;
e si ritrova a scoprire (o a riscoprire) che costruendo un oggetto di classe D viene chiamata B::Init e non D::Init.
In effetti, durante la costruzione di un oggetto (che come sapete richiede la chiamata ai costruttori di tutta la gerarchia) il dispatch delle funzioni virtuali avviene in base al tipo della classe completamente costruita al momento della chiamata, non al tipo della classe piu' derivata (che puo' non essere ancora completamente costruita).
La ragione principale per tale comportamento e' che chiamare una funzione di una classe il cui costruttore non e' ancora stato chiamato e' troppo pericoloso, perche' non possiamo assumere alcun valore per i data member di quella classe (anche se il linguaggio li forzasse a zero, cosa che non fa, il valore assunto potrebbe comunque non rispettare l'invariante della classe).

D'altra parte, chiamare una funzione virtuale in un costruttore puo' essere utile: siccome in teoria "non si puo'", molte librerie impongono agli utilizzatori una chiamata di costruzione "doppia", ad esempio (in MFC):

CWnd w ; // costruttore di default, che fa poco/niente
w.Create( ..... ) ; // costruzione "vera"
all'interno di Create le funzioni virtuali sono soggette ad un vero dispatch dinamico, e questa e' una delle ragioni per cui in MFC la vera costruzione e' fatta in Create e non nel costruttore: altrimenti, alcuni messaggi non sarebbero gestibili nelle classi derivate.
Lo stesso problema si presenta in librerie con classi Thread, che richiedono di derivare la nostra classe da Thread, costruire un oggetto, e poi farlo partire con Start(). Se mettessimo Start() nel costruttore di Thread, non verrebbe sempre invocata la funzione di classe derivata ma quella di classe base (probabilmente virtuale pura!).

"Non si puo'" non e' ammissibile :-)
Al di la' dello spirito di No Limits, per cui non e' accettabile prendere atto di una limitazione e dire "pazienza", costringere gli utilizzatori di una libreria a costruire l'oggetto e poi a chiamare Create e' decisamente brutto, perche' incoraggia uno stile di codifica facilmente soggetto ad errori. D'altra parte, e' evidente che per superare il problema bisogna "aspettare" che tutti i costruttori per l'intera gerarchia vengano chiamati, e poi chiamare le funzioni virtuali di cui vuole un vero dispatch dinamico.
In generale, vorremmo creare un meccanismo con le seguenti caratteristiche:

1) Totale trasparenza lato utilizzatore: chi usa le classi non deve chiamare funzioni aggiuntive o simili. Semplicemente costruisce il suo oggetto.

2) Totale sicurezza lato utilizzatore: chi usa le classi non puo' usarle in modo errato, ad es. facendo si' che le funzioni virtuali "posticipate" non vengano chiamate.

3) Totale controllo lato fornitore: chi scrive la classe base deve poter decidere quali funzioni virtuali vengono chiamate, ed in quale sequenza. Con un unico vincolo (che e' al contempo una garanzia): tali funzioni verranno chiamate dopo la costruzione completa dell'oggetto di classe piu' derivata.

4) Relativa semplicita' di estensione: chi estende la classe base "furba" di cui sopra non dovrebbe essere costretto a scrivere codice intricato. In fondo, vogliamo chiamare le funzioni virtuali nella classe base proprio per evitare a chi deriva di riscrivere le invocazioni nel costruttore (anche per le ovvie ragioni di manutenzione).

5) Totale sicurezza di estensione: ovvero, anche se si richiede un minimo di fatica a chi estende la classe base, vorremmo che il meccanismo fosse robusto, e che in caso di dimenticanze generasse un errore di compilazione.

Tutti i punti possono essere soddisfatti, rimanendo all'interno dello standard ed usando un po' di creativita'. Ovviamente il punto (4) e' soggettivo, quindi stara' a voi decidere se la complessita' aggiunta per chi deriva nuove classi e' ragionevole o meno. A mio avviso lo e'. La soluzione finale non e' banale, ma e' possibile arrivarci per passi seguendo un processo tutto sommato semplice.

Passo a: l'affidabile temporaneo
Un elemento di standardizzazione importante riguarda la vita dei temporanei. Se prima dello standard ANSI/ISO la vita di un temporaneo aveva una definizione ambigua, che permetteva varianti implementative, lo standard ha sancito che un temporaneo deve vivere esattamente sino alla fine della espressione completa in cui appare.
Da cio' possiamo dedurre che un temporaneo introdotto nella chiamata ad un costruttore verra' distrutto esattamente dopo la costruzione dell'oggetto. In particolare, nel caso seguente:

class I
  {
  } ;

class B
  {
  public :
    B( I i = I() ) {}
  } ;
 
int main()
  {
  B b ;
  return( 0 ) ;
  }
l'oggetto temporaneo di classe I verra' costruito prima di iniziare la costruzione dell'oggetto di classe B (perche' i parametri vanno valutati prima di iniziare l'esecuzione della funzione chiamata) e distrutto immediatamente dopo la costruzione di b (e prima della distruzione di b), proprio in virtu' di quanto detto sopra.

Possiamo quindi pensare di sfruttare la lifetime del temporaneo per invocare una funzione virtuale (come Init) esattamente dopo la costruzione dell'oggetto, semplicemente chiamandola dal distruttore di un oggetto temporaneo introdotto nel costruttore della classe da inizializzare (whew:-) :


class Base ;

class Initializer
  {
  public :
    Initializer() 
      { 
      p = 0 ;
      }
    ~Initializer() ; // see below
    void Register( Base* b )
      {
      p = b ;
      }
  private :
    Base* p ;
  } ;

class Base
  {
  public :
    Base( Initializer i = Initializer() ) 
      { 
      i.Register( this ) ;
      }
    virtual void Init() 
      {
      // ... something
      }
  } ;

Initializer :: ~Initializer()
  { 
  if( p )
    p->Init() ; // call the virtual function
  }

int main()
  {
  Base b ;  // transparently calls Base::Init after constructing b
  return( 0 ) ;
  }
La soluzione rispetta il punto (1) visto sopra, ovvero la totale trasparenza per chi usa la classe. Rispetta anche il punto (3), perche' scrivendo nel modo opportuno il distruttore di Initializer possiamo chiamare, nell'ordine desiderato, qualunque funzione di Base.
Purtroppo rispettare gli altri punti non e' cosi' semplice: vi sono infatti diversi elementi che meritano un approfondimento.

Intanto, creiamo inutilmente un ulteriore temporaneo di classe Initializer, che poi viene copiato nel parametro i. Questo e' un difetto minore, con un impatto che potremmo dire trascurabile sulle prestazioni.

In secondo luogo, l'utilizzatore potrebbe sbagliarsi e scrivere codice come il seguente (magari fuorviato dall'header della classe Base):

int main()
  {
  Initializer i ;
  Base b( i ) ;
  return( 0 ) ;
  }
Questo codice non funziona a dovere, perche' non chiama Init. Anche altre varianti, dove l'utilizzatore chiama Register in modo esplicito, non si comportano bene. La soluzione non rispetta quindi il punto (2) di cui sopra.

In terzo luogo, chi estende la classe Base deve ricordarsi di creare a propria volta un temporaneo e di chiamare Register, come segue:

class Derived : public Base
  {
  public :
    Derived( Initializer i = Initializer() ) 
      { 
      i.Register( this ) ;
      }
    virtual void Init() 
      {
      // ... something
      }
  } ;
Capire le ragioni e le conseguenze di quest'ultimo punto non e' banale, ma e' fondamentale per trovare una soluzione migliore.
La ragione principale e' che scrivendo la classe derivata come segue:
class Derived : public Base
  {
  public :
    Derived() // implicit or explicit:  : Base()
      { 
      }
    virtual void Init() 
      {
      // ... something
      }
  } ;

il temporaneo verrebbe creato nell'espressione completa "sbagliata", ovvero in Base() e non nel punto di istanziazione di Derived. Di conseguenza, Init verrebbe chiamata dopo la costruzione di Base ma prima della costruzione di Derived, vanificando lo sforzo fatto.
Le conseguenze fondamentali sono 3: la classe derivata diventa piu' complicata da scrivere (andiamo contro il punto 4), Init deve usare un flag per proteggersi da chiamate multiple, ed infine non si rispetta il punto (5) dato sopra, perche' e' sempre possibile sbagliare e scrivere Derived come sopra, anziche' "nel modo giusto".
Vista cosi', la soluzione impostata e' quindi molto fragile, e non rispetta quasi nessuno dei punti che avevamo fissato inizialmente. Dobbiamo lavorarci su parecchio prima di avere un codice accettabile.

Passo b: reference a temporaneo
Uno dei problemi piu' fastidiosi della versione precedente e' la creazione di temporanei multipli nel caso di derivazione, con la conseguente necessita' di proteggersi da invocazioni multiple di Init e di chiamare Register anche nelle classi derivate. Infatti, Derived crea il suo temporaneo come parametro di default, e Base ne crea un altro, o come default, o tramite costruttore di copia in quanto il parametro viene passato a Base per valore.
Questo non avverrebbe se la signature di Base::Base fosse:
Base( const Initializer& i = Initializer() ) 

perche' in tal caso Derived potrebbe creare un solo temporaneo e passarlo al costruttore di Base. Avremmo anche un vantaggio collaterale, ovvero eviteremmo il minimo impatto sulle prestazioni dovuto alla creazione di temporanei multipli.
Notate che il reference deve essere const, perche' lo standard impone che un temporaneo possa essere legato solo a riferimenti a costante. Siccome il temporaneo viene successivamente modificato (chiamando la funzione Register), il codice di Initializer andrebbe rivisto: Register deve diventare const, ed il puntatore p deve diventare mutable.
Questo risolverebbe il problema di dover chiamare Init anche nel costruttore di Derived, e quindi di dover proteggere Init, ma non risolverebbe le violazioni dei punti (2) e (5) visti sopra. Entrambi i punti richiedono un po' di creativita' per essere risolti.

Passo c: default e binding
Una caratteristica interessante dei parametri di default e' il modo in cui viene verificata l'accessibilita' del loro costruttore. Il punto 11.0.7 dello standard recita infatti: "The names in a default argument expression are bound at the point of declaration, and access is checked at that point rather than at any points of use of the default argument expression".
Tradotto in un linguaggio piu' comprensibile :-), questo significa che nel nostro caso l'accessibilita' del costruttore di Initializer viene controllata rispetto alla classe Base, non rispetto al punto in cui istanziamo gli oggetti di classe Base (o Derived).
Questo suggerisce una variante molto utile: se spostiamo la classe Initializer dentro la classe Base (nella sezione protected), Initializer sara' accessibile a Base ed a Derived, ma non all'utilizzatore di tali classi. Questo impedirebbe all'utilizzatore di usare erroneamente Initializer, soddisfacendo cosi' il punto (2) dato sopra.
Vediamo un esempio di come si potrebbe implementare la variante con temporaneo passato per reference (passo b) e con classe nested protetta:
class Base
  {
  protected :
    // next 2 lines necessary for conforming compilers
    class Initializer ;
    friend class Initializer ;
    // nested initializer class
    class Initializer
      {
      public :
        Initializer() 
          { 
          p = 0 ;
          }
        ~Initializer() 
          { 
          if( p )
            p->Init() ; // call the virtual function
          }
        void Register( Base* b ) const 
          {
          p = b ;
          }
      private :
        mutable Base* p ;
      } ;
  public :
    Base( const Initializer& i ) 
      { 
      i.Register( this ) ;
      }
  protected :
    virtual void Init() 
      { 
      // .... something ; 
      }
  } ;

A questo punto, codice come il seguente:
int main()
  {
  Initializer i ;
  Base b( i ) ;
  return( 0 ) ;
  }

diventa illegale, mentre l'uso "corretto" della classe
int main()
  {
  Base b ;
  return( 0 ) ;
  }

rimane naturalmente valido. Notate che ho anche approfittato del nesting per rendere Initializer friend di Base, e spostare Init nella sezione protected: evitiamo cosi' che gli utilizzatori possano chiamare Init indebitamente.
Resta il problema di impedire a chi scrive le classi derivate di "dimenticare" la creazione del temporaneo, problema che sembra apparentemente inscindibile dall'utilizzo di un valore di default. D'altra parte, il valore di default e' proprio cio' che ci ha consentito di soddisfare i punti (1) e (2), e non vorremmo proprio ricominciare da capo.

Passo d: vincoli ed ereditarieta' virtuale
Nel complesso armamentario del C++, l'ereditarieta' virtuale si colloca tra gli aspetti meno compresi da molti programmatori. Per una discussione approfondita vi rimando ad [1], ma vi ricordo qui un fattore fondamentale: nel caso di ereditarieta' virtuale, il costruttore della classe base virtuale deve essere chiamato esplicitamente dai costruttori delle classi piu' derivate. Non importa se classi intermedie lo chiamano, e se i costruttori delle classi piu' derivate chiamano quelli delle classi intermedie: sono comunque tenuti a chiamare esplicitamente anche il costruttore della classe base virtuale.
Questo porta all'idea finale: estrarre una classe base virtuale da cui derivare Base. La classe base virtuale si occupera' della registrazione, eliminando da Base questo compito: Base tornerebbe quindi ad occuparsi solo di quanto le compete realmente. Tuttavia, la classe base virtuale richiedera' un reference ad Initializer, e nel suo costruttore non avra' un valore di default. Dovranno quindi essere le classi derivate (inclusa Base) a passare un temporaneo. Poiche' tutti i costruttori delle classi derivate devono chiamare il costruttore della classe base virtuale, questo impedira' di non creare il temporaneo nelle classi piu' derivate. Non solo, siccome e' il linguaggio a garantire che il costruttore della classe base virtuale venga chiamato una volta sola (in corrispondenza della chiamata da parte della classe piu' derivata), avremo anche la garanzia sulla lifetime del temporaneo su cui la classe base virtuale chiamera' Register.
Tradotto in codice, quanto sopra diventa:

class VirtualBase
  {
  protected :
    class Initializer ;
    friend class Initializer ;
    class Initializer
      {
      public :
        Initializer() 
          { 
          std::cout << "Initializer" << std::endl ; 
          p = 0 ;
          }
        ~Initializer() 
          { 
          std::cout << "~Initializer" << std::endl ; 
          if( p )
            p->Init() ; // call the virtual function
          }
        // might be private if VirtualBase is declared as friend...
        void Register( VirtualBase* b ) const 
          {
          p = b ;
          }
      private :
        mutable VirtualBase* p ;
        // private and not implemented
        Initializer( const Initializer& ) ;
        void operator =( const Initializer& ) ;
      } ;
  public :
    VirtualBase( const Initializer& i ) 
      { 
      std::cout << "VirtualBase" << std::endl ; 
      i.Register( this ) ;
      }
  private :
      virtual void Init() = 0 ; 
      // will be called immediately after the constructor 
      // of the most derived class
  } ;

// This is the actual hierarchy

class Base : public virtual VirtualBase
  {
  public :
    Base( const VirtualBase::Initializer& i = Initializer() ) : VirtualBase( i ) 
      { 
      std::cout << "Base" << std::endl ; 
      }
    ~Base() 
      { 
      std::cout << "~Base" << std::endl ; 
      }
    void Init() 
      { 
      std::cout << "Base::Init()" << std::endl ; 
      }
  } ;


class Derived : public Base
  {
  public :
    //  Derived() : Base() {} // compile-time error as wanted
    Derived( const VirtualBase::Initializer& i = Initializer() ) : 
      Base( i ), VirtualBase( i )  // Base( i ) is optional...
      { 
      std::cout << "Derived" << std::endl ; 
      }
    ~Derived() 
      { 
      std::cout << "~Derived" << std::endl ; 
      }
    void Init() 
      { 
      std::cout << "Derived::Init()" << std::endl ; 
      }
  } ;


int main()
  {
  Base x1 ;  // calls Base::Init
  Derived y ; // calls Derived::Init
  return( 0 ) ;
  }
Ho aggiunto alcuni statement di trace per consentire a chi voglia di seguire meglio le varie fasi. Rispetto a quanto detto prima, ho anche migliorato Initializer, definendo un copy constructor ed un operatore di assegnazione privati e non implementati: non vogliamo che gli Initializer vengano copiati. Notiamo che la soluzione data sopra rispetta tutti i punti 1-5. In particolare, il punto (4) e' stato sinora trascurato. Nel caso in questione, scrivere una classe derivata richiede solo di aggiungere una chiamata al costruttore di VirtualBase nel costruttore della derivata: uno sforzo che ritengo minimo, considerato che il compilatore ci impedira' di dimenticarlo. Esiste comunque un modo per scrivere una classe derivata "sbagliata": dichiarare un data member statico di classe Initializer nella classe derivata, e passarlo al costruttore di VirtualBase. Dovra' quindi essere ben chiaro a chi scrive le classi derivate che Initializer va solo usato come temporaneo nella chiamata al costruttore di VirtualBase. Nuovamente, credo sia una situazione accettabile, considerando che chi estende le classi ha comunque un rapporto privilegiato (un accoppiamento piu' forte) con la classe base. Chi utilizza Base e Derived non ha invece alcuna possibilita' di sbagliare. Una sola nota finale: per semplificare l'esempio, Base e Derived non prendono altri parametri nel costruttore. Ovviamente la tecnica e' ugualmente applicabile se esistono dei parametri, basta mettere quello di tipo Initializer come ultimo parametro della lista. Possiamo anche cambiarne il nome da "i" a qualcosa come "dontPassThis" per indicare a chi si legge il .H (invece della documentazione :-) che il parametro non va passato dall'utilizzatore (che comunque non puo' farlo).

Conclusioni
Il meccanismo completo non e' proprio semplice, ed e' costruito basandosi su tre aspetti distinti (lifetime dei temporanei, accessibilita' del costruttore dei parametri di default, ereditarieta' virtuale e costruttori) che "mescolati" nel modo giusto portano al risultato voluto.
Come sempre, da un esercizio No Limits possiamo trarre alcuni insegnamenti.
Il primo punto su cui vorrei soffermarmi riguarda, al solito, l'estrema flessibilita' del C++. I costrutti disponibili sono relativamente low-level se confrontati con altri linguaggi, e sono piu' numerosi rispetto a linguaggi piu' high-level o piu' minimalisti. Per un programmatore esperto, tuttavia, sono sorprendentemente "giusti" nella loro granularita' ed ortogonalita', perche' consentono di essere combinati in modi nuovi ed originali.
Il secondo punto e' che talvolta possiamo usare un costrutto non per cio' che significa, ma per cio' che fa. Non posso non mettere in guardia dal pericolo insito in questa affermazione: di norma, mi aspetto di vedere l'ereditarieta' utilizzata per esprimere una relazione IS-A, non come un trucco per forzare un comportamento particolare delle classi derivate. Tuttavia, in casi come quello presentato, "staccare" il significato dal comportamento ci consente di espandere lo spettro delle soluzioni, a tutto vantaggio di cui utilizza poi le classi prodotte.
Il terzo punto riguarda infatti la progettazione di librerie. Al di la' dell'esercizio in questione, molte librerie sono scomode da usare perche' chi le ha progettate non si e' curato molto di alcuni aspetti cruciali: la semplicita' di uso, ma anche la sicurezza nell'utilizzo, sono tra questi. E' curioso come elementi che si ritengono essenziali nella progettazione di un prodotto vengano giudicati come trascurabili quando si progetta una libreria. Invece una libreria e' un prodotto come gli altri, con un gruppo di utenti particolare, ma che merita lo stesso rispetto dei canoni di interazione uomo-macchina dei normali prodotti software.
Molto spesso una libreria semplice e sicura da usare, e ragionevolmente semplice da estendere, ha una struttura interna piu' complessa di una libreria equivalente, che non sia stata pero' pensata per "nascondere la complessita'", e che la esponga quindi ai suoi utilizzatori. Questo principio (information hiding) e' invece alla base di ogni buon progetto, librerie e framework inclusi. Per un framework in particolare, non esporre gli utilizzatori a difficolta' eccessive dovrebbe sempre essere uno degli obiettivi principali del design.

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.