Dr. Carlo Pescio
Un "forName/newInstance" in C++

Pubblicato su C++ Informer No. 2, Gennaio 1998

Questo articolo nasce dalla richiesta di uno dei partecipanti al mio worklab sul Systematic Object Oriented Design al recente Developer's Forum. In particolare, discutendo i vincoli di istanziazione, avevo fatto notare come in alcuni linguaggi si possa creare un oggetto avendo solo il nome della classe come stringa. Ad esempio in Java [1] possiamo scrivere:

string s = "pippo" ;
Class c = Class.forName( s ) ;
Object o = c.newInstance() ;

e questo ha lo stesso effetto di:
Object o = new pippo() ;

Ovviamente la prima forma e' molto piu' flessibile, perche' la stringa puo' anche essere letta da file, da database, o che altro; e' anche piu' lenta, ma non sempre questo e' un problema reale.
Ho poi fatto notare che con un po' di fatica e' possibile ottenere qualcosa di simile anche in C++, ed a seguito di alcune domande siamo scesi in un minimo di dettagli implementativi. Avevo promesso di occuparmi piu' a fondo del problema su C++ Informer, ed infatti ora discutero' una soluzione completa ed il piu' possibile automatizzata. In pratica, saremo in grado di scrivere codice del tutto analogo a quello Java, purche' la nostra classe (pippo) sia stata dichiarata in modo opportuno. Per semplificare ulteriormente, potremo anche usare una macro di appoggio per la dichiarazione.

Idee di base
Per implementare una funzionalita' simile al forName/newInstance di Java in C++, dobbiamo sostanzialmente mantenere una mappa dal nome delle classi a delle funzioni in grado di creare oggetti di ogni classe. Dopodiche' newInstance non fara' altro che accedere alla mappa, trovare la funzione di creazione e chiamarla. Tutto il problema sta nel rendere la creazione della mappa il piu' semplice possibile, senza richiedere a chi implementa le varie classi di registrarsi manualmente, di implementare delle funzioni di creazione, eccetera.
Iniziamo a vedere come possiamo "iniettare" una funzione di creazione dentro una classe: questa funzione di creazione verra' poi inserita all'interno della mappa. Siccome la funzione di creazione deve creare oggetti di classe qualunque, e' necessario ricorrere ad un template:

template< class T > class Registered
  {
  public :
    static void* newInstance() 
      {
      return( new T() ) ;
      }
  } ;
A questo punto, Registered< pippo > ha una funzione newInstance che crea oggetti di classe pippo. Se dichiariamo pippo come segue:

class pippo : public Registered< pippo >
  {
  // ...
  } ;
otteniamo come risultato una classe pippo con un metodo statico newInstance che crea nuovi oggetti di classe pippo.
Dobbiamo ora registrare questo metodo statico in una mappa, associandogli il nome della classe (come stringa). In teoria, potremmo pensare di estrarre il nome della classe direttamente dentro il template Registered, usando il RTTI:

string nameT( typeid( T ).name() ) ;
Tuttavia, questo in genere non da' il risultato sperato. Secondo lo standard, la funzione type_info::name() restituisce una stringa implementation-defined. Non necessariamente si tratta di una stringa leggibile, tanto meno del nome della classe. Invece, noi vogliamo scrivere proprio un codice Java-style, dove per creare oggetti di classe pippo scriviamo "pippo".
Ovviamente, la soluzione e' passare manualmente il nome della classe; vedremo poi come automatizzare questo passo con l'uso delle macro. Il nostro template Registered diventa quindi:

template< class T, const char S[] > class Registered
  {
  public :
    static void* newInstance() 
      {
      return( new T() ) ;
      }
  private :
    static const RegEntry r ;
  } ;

template< class T, const char S[] > const RegEntry
Registered< T, S > :: r( S, Registered< T, S >::newInstance ) ;
L'oggetto statico di classe RegEntry serve ad inserire nella mappa la coppia <nome della classe, funzione di creazione>. Vedremo tra breve come svolga questo compito. Prima, tuttavia, e' importante notare che per istanziare un template con un parametro puntatore, l'oggetto puntato deve necessariamente avere link esterno (paragrafo 14.1.3 dello standard). Quindi non potremo dichiarare pippo come segue:

class pippo : public Registered< pippo, "pippo" >
  {
  // ...
  } ;

ma dovremo necessariamente scrivere qualcosa del tipo:

char pippoName = "pippo" ;
class pippo : public Registered< pippo, pippoName >
  {
  // ...
  } ;

Comunque, sara' sempre l'uso delle macro a semplificare questa parte di codice.

A seguito di una dichiarazione come quella sopra, esistera' una funzione statica newInstance() che crea oggetti di classe pippo, ed un oggetto statico di classe RegEntry. Lo scopo di questo oggetto statico e' semplicemente di registrare automaticamente la coppia <nome della classe, funzione di creazione> nella succitata mappa:

typedef void* (*FactoryMethod)() ;

class RegEntry 
  {
  public :
    RegEntry( const char s[], FactoryMethod f ) 
      { 
      Class::Register( s, f ) ;
      }
  } ;
FactoryMethod e' il tipo di un puntatore ad una funzione di creazione. Quando creiamo l'oggetto statico di classe RegEntry, questo non fa altro che registrare il nome della classe ed il metodo newInstance della classe stessa nella mappa, mantenuta dalla classe Class che vedremo tra breve. L'uso di un oggetto statico creato dal template Registered ci assicura che la registrazione della nostra classe pippo avverra' in modo automatico, senza doverlo richiedere, allo startup del programma.
La classe Class, che ho mantenuto sullo stile di Java, ha il compito di mantenere la mappa e di creare su richiesta nuovi oggetti. Per ovvie ragioni, ho deciso di implementare la mappa attraverso la classe map di STL:

class Class
  {
  public :
    static Class forName( string s ) ;
    void* newInstance() ;
  private :
    string name ;
    typedef map< string, FactoryMethod >
    FactoryMethodMap ;
    static FactoryMethodMap* fmMap ;
    friend class RegEntry ;
    static void Register( string s, FactoryMethod m ) ;
    Class( string s ) ;
  } ;
Prima di scendere nei dettagli di implementazione di Class, vorrei farvi notare che la sua interfaccia pubblica e' stata scelta per rispecchiare esattamente quella di Java. Se abbiamo dichiarato pippo come sopra, possiamo scrivere:

string s = "pippo" ;
Class c = Class::forName( s ) ;
void* o = c.newInstance() ;
L'unica differenza e' l'uso di "Class::" anziche' di "Class." per chiamare la funzione statica forName (a causa di una differenza sintattica tra C++ e Java) e l'uso di un void* anziche' di Object, visto che in C++ non esiste una radice unica. Nei casi di utilizzo concreto, al posto di void* useremo probabilmente una classe interfaccia, comune a tutte le classi istanziate.
Torniamo ora all'implementazione di Class. Come vedete esiste un data member statico fmMap, che punta alla mappa. L'uso di un puntatore e' fondamentale: poiche' questo oggetto viene utilizzato dal costruttore di altri oggetti statici (tutti i RegEntry), dobbiamo essere certi che la mappa sia gia' stata costruita quando costruiamo i RegEntry. Purtroppo lo standard non da' alcuna garanzia circa l'ordine di costruzione di oggetti statici in differenti unita' di compilazione. La soluzione e' in questo caso abbastanza semplice. Usando un puntatore, lo standard ci garantisce che verra' inizializzato a zero prima della costruzione degli oggetti statici. A questo punto la funzione Class::Register (chiamata dal costruttore dei RegEntry) diventa:

void Class :: Register( string s, FactoryMethod m )
  {
  if( fmMap == NULL )
    fmMap = new map< string, FactoryMethod > ;
  fmMap->insert( pair< string, FactoryMethod >( s, m ) ) ;
  }
Ovvero, la mappa viene creata al momento della prima chiamata, mentre in tutti i casi viene inserita una nuova coppia. Questo impone un piccolo overhead, ma d'altra parte, la registrazione avviene solo all'avvio del programma (manca un controllo su eventuali errori di duplicazione, peraltro facile da aggiungere).
Notiamo che fmMap non viene mai distrutto. Questo e' un classico problema dei singleton [2], che si poteva risolvere usando un auto_ptr anziche' un puntatore normale. Purtroppo un paio di compilatori sui cui ho provato il codice davano una serie di problemi (legati in realta' ai namespace), per cui ho preferito lasciare il puntatore "nudo". In fondo, la mappa deve persistere sino alla fine del programma, e su un sistema operativo decente dopo la terminazione del programma la memoria viene comunque riciclata (in alternativa si poteva utilizzare un altro oggetto statico per distruggere fmMap). Da notare anche l'uso di friend: solo gli oggetti di classe RegEntry hanno il permesso di registrare nuove classi. In casi come questo, l'uso attento di friend aumenta l'incapsulazione.
Vediamo ora l'implementazione del costruttore, di forName e newInstance. Iniziamo da forName: come in Java, questo deve restituire un oggetto di classe Class "associato" alla stringa passata come parametro. Nella nostra implementazione, non deve far altro che restituire un oggetto Class costruito al volo:

static Class Class :: forName(string s )
  {
  return( Class( s ) ) ;
  }

dove il costruttore non fa altro che memorizzare il nome della classe:
Class :: Class(string s ) : name( s ) 
  {
  }

A questo punto creare un oggetto e' piuttosto semplice, basta trovare la funzione di creazione nella mappa e chiamarla:

void* Class :: newInstance()
  {
  assert( fmMap != NULL ) ;
  FactoryMethodMap::iterator it = fmMap->find( name ) ;
  if( it != fmMap->end() )
    return( it->second() ) ;
  else
    return( 0 ) ;
  }

Zucchero sintattico

L'uso di Class, forName e newInstance e' ora del tutto analogo a quello di Java: manca solo il supporto per i tipi predefiniti, peraltro semplice da aggiungere. L'unica differenza, a dire il vero un po' fastidiosa, sta proprio nella necessita' di registrare la classe, definire una stringa, eccetera. A questo si puo' ovviare con una macro come la seguente:

#define REG_CLASS( C ) \
char C##Name__[] = #C ; \
class C : public Registered< C, C##Name__ >

Da utilizzare come segue:
REG_CLASS( pippo )
  {
  // ... 
  } ;

La macro si preoccupa di definire una stringa per il nome e derivare da Registered con i giusti parametri. L'unico problema e' che la macro non si puo' utilizzare nel caso di classi nested, in quanto la stringa verrebbe definita localmente alla classe immediatamente piu' esterna. Peraltro, in questi casi dobbiamo anche "inventarci" un nome piu' lungo, tenendo conto del nesting. Fortunatamente, le classi nested sono di uso abbastanza raro ed in quei casi possiamo sempre scrivere il relativo codice manualmente, senza usare la macro.

Problemi di compilatore...
Siccome si tratta di un codice abbastanza utile, ho cercato di adattarlo almeno ai compilatori che uso piu' di frequente, ed anche all'ecgs (vedere sotto "News ed Annunci"). Ho riscontrato due problemi fondamentali, entrambi legati al data member statico r della classe Registered. In pratica, su molti compilatori non veniva generato il codice relativo; indubbiamente, una conseguenza di una interpretazione un po' troppo zelante della regola secondo cui le parti di un template che non vengono referenziate non devono essere generate.
Per convincere il Visual C++ 5.0 a generare il codice associato ad r (ovvero la chiamata al costruttore di RegEntry), occorreva in qualche modo referenziare r. Apparentemente bisognava farlo fuori dal template Registered, altrimenti avremmo solo spostato il problema di un livello: se non referenziavamo la funzione che referenziava r, la funzione non veniva generata e quindi r risultava comunque non referenziato. Una bella seccatura, perche' volevo evitare alle classi derivate da Registered (come pippo) di dover chiamare qualche funzione della classe base: in fondo, usavo un oggetto statico proprio per evitare di chiamare una funzione di inizializzazione.
Alla fine ho trovato una soluzione "creativa": aggiungere un costruttore di default protected a Registered, che referenzia in modo fasullo r:

template< class T, const char S[] > class Registered
  {
  // ...
  protected :
    Registered()
      {
      const RegEntry& dummy = r ;
      }
  // ...
  } ;
Il costruttore di default viene automaticamente referenziato dal costruttore delle classi derivate, quindi viene sempre generato il relativo codice. Il data member r risulta quindi referenziato e quindi anche il suo codice di costruzione viene generato. Da notare che questo impone un piccolo overhead di costruzione per tutti gli oggetti di classe derivata (come pippo). Fortunatamente, qualunque compilatore di qualita' non infima si accorge che il reference "dummy" non e' usato fuori dal costruttore protected, ed elimina quel codice in fase di ottimizzazione.
Infine, per il Visual C++ la sintassi di definizione di r data sopra e' sbagliata (probabilmente un errore nel parser), ed e' necessario riaggiustarla come segue:
template< class T, const char S[] > const RegEntry
Registered< T, S > :: r = RegEntry( S, Registered< T, S >::newInstance ) ;
Nessun problema, comunque, perche' il costruttore di copia di RegEntry non fa nulla, e la registrazione avviene comunque attraverso il costruttore "normale".

Listato completo
Il seguente listato riporta una implementazione completa del codice discusso sopra:

// CLASS.H

#ifndef CLASS__


#define CLASS__


#include <string>
#include <map>
#include <memory>


typedef void* (*FactoryMethod)() ;

class RegEntry ;

class Class
  {
  public :
    static Class forName( std::string s )
      {
      return( Class( s ) ) ;
      }
    void* newInstance() ;
  private :
    std::string name ;
    typedef std::map< std::string, FactoryMethod > FactoryMethodMap ;
    static FactoryMethodMap* fmMap ;
    friend class RegEntry ;
    static void Register( std::string s, FactoryMethod m ) ;
    Class( std::string s ) : name( s ) 
      {
      }
  } ;


class RegEntry 
  {
  public :
    RegEntry( const char s[], FactoryMethod f ) 
      { 
      Class::Register( s, f ) ;
      }
  } ;


template< class T, const char S[] > class Registered
  {
  public :
    static void* newInstance() 
      {
      return( new T() ) ;
      }
  protected :
    Registered()
      {
      const RegEntry& dummy = r ;
      }
  private :
    static const RegEntry r ;
  } ;

template< class T, const char S[] > const RegEntry 
Registered< T, S > :: r = RegEntry( S, Registered< T, S >::newInstance ) ;

#define REG_CLASS( C ) \
char C##Name__[] = #C ; \
class C : public Registered< C, C##Name__ >

#endif



// Class.CPP

#include <assert.h>
#include "class.h"

using namespace std ;

Class :: FactoryMethodMap* Class :: fmMap ;


void* Class :: newInstance()
  {
  assert( fmMap != NULL ) ;
  FactoryMethodMap::iterator it = fmMap->find( name ) ;
  if( it != fmMap->end() )
    return( it->second() ) ;
  else
    return( 0 ) ;
  }


void Class :: Register( std::string s, FactoryMethod m )
  {
  if( fmMap == NULL )
    fmMap = new std::map< std::string, FactoryMethod > ;
    fmMap->insert( std::pair< std::string, FactoryMethod >( s, m ) ) ;
  }


// TEST.CPP

#include <iostream>
#include <typeinfo>
#include "class.h"

using namespace std ;

REG_CLASS( Pippo )
  {
  public :
    Pippo() { cout << "Pippo()" << endl ; }
  } ;

REG_CLASS( Pluto )
  {
  public :
    Pluto() { cout << "Pluto()" << endl ; }
  } ;

int main()
  {
  Class c1 = Class::forName( "Pippo" ) ;
  void* p1 = c1.newInstance() ; 
  Class c2 = Class::forName( "Pluto" ) ;
  void* p2 = c2.newInstance() ;
  return( 0 ) ;
  }


Bibliografia
[1] Arnold, Gosling, "The Java Programming Language", Addison-Wesley, 1996.
[2] John Vlissides, "To Kill a Singleton", C++ Report, June 1996.

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.