|
Dr. Carlo Pescio Un "forName/newInstance" in C++ |
string s = "pippo" ; Class c = Class.forName( s ) ; Object o = c.newInstance() ;
Object o = new pippo() ;
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.
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".
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 >
{
// ...
} ;
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.
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.
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).
static Class Class :: forName(string s )
{
return( Class( s ) ) ;
}
Class :: Class(string s ) : name( s )
{
}
void* Class :: newInstance()
{
assert( fmMap != NULL ) ;
FactoryMethodMap::iterator it = fmMap->find( name ) ;
if( it != fmMap->end() )
return( it->second() ) ;
else
return( 0 ) ;
}
Zucchero sintattico#define REG_CLASS( C ) \ char C##Name__[] = #C ; \ class C : public Registered< C, C##Name__ >
REG_CLASS( pippo )
{
// ...
} ;
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.
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
// 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.