Registra dynamicmente i metodi di costruzione in un AbstractFactory in fase di compilazione utilizzando i modelli C ++

Quando si implementa una class MessageFactory per instaziare gli oggetti Message, ho usato qualcosa come:

class MessageFactory { public: static Message *create(int type) { switch(type) { case PING_MSG: return new PingMessage(); case PONG_MSG: return new PongMessage(); .... } } 

Funziona bene ma ogni volta che aggiungo un nuovo messaggio devo aggiungere un nuovo XXX_MSG e modificare l’istruzione switch.

Dopo alcune ricerche ho trovato un modo per aggiornare dynamicmente il MessageFactory in fase di compilazione in modo da poter aggiungere tutti i messaggi che voglio senza dover modificare la stessa MessageFactory. Ciò consente di mantenere un codice più pulito e più semplice in quanto non è necessario modificare tre diverse posizioni per aggiungere / rimuovere classi di messaggi:

 #include  #include  #include  #include  class Message { protected: inline Message() {}; public: inline virtual ~Message() { } inline int getMessageType() const { return m_type; } virtual void say() = 0; protected: uint16_t m_type; }; template class MessageTmpl: public Message { enum { _MESSAGE_ID = TYPE }; public: static Message* Create() { return new IMPL(); } static const uint16_t MESSAGE_ID; // for registration protected: MessageTmpl() { m_type = MESSAGE_ID; } //use parameter to instanciate template }; typedef Message* (*t_pfFactory)(); class MessageFactory⋅ { public: static uint16_t Register(uint16_t msgid, t_pfFactory factoryMethod) { printf("Registering constructor for msg id %d\n", msgid); m_List[msgid] = factoryMethod; return msgid; } static Message *Create(uint16_t msgid) { return m_List[msgid](); } static t_pfFactory m_List[65536]; }; template  const uint16_t MessageTmpl::MESSAGE_ID = MessageFactory::Register( MessageTmpl::_MESSAGE_ID, &MessageTmpl::Create); class PingMessage: public MessageTmpl  {⋅ public: PingMessage() {} virtual void say() { printf("Ping\n"); } }; class PongMessage: public MessageTmpl  {⋅ public: PongMessage() {} virtual void say() { printf("Pong\n"); } }; t_pfFactory MessageFactory::m_List[65536]; int main(int argc, char **argv) { Message *msg1; Message *msg2; msg1 = MessageFactory::Create(10); msg1->say(); msg2 = MessageFactory::Create(11); msg2->say(); delete msg1; delete msg2; return 0; } 

Il modello qui fa la magia registrandosi nella class MessageFactory, tutte le nuove classi Message (ad esempio PingMessage e PongMessage) che sottoclass da MessageTmpl.

Funziona perfettamente e semplifica la manutenzione del codice, ma ho ancora alcune domande su questa tecnica:

  1. È una tecnica / modello noto? qual è il nome? Voglio cercare più informazioni su di esso.

  2. Voglio rendere la matrice per la memorizzazione di nuovi costruttori MessageFactory :: m_List [65536] a std :: map ma facendo ciò, il programma segusterà anche prima di raggiungere main (). La creazione di un array di 65536 elementi è eccessivo, ma non ho trovato un modo per rendere questo un contenitore dinamico.

  3. Per tutte le classi di messaggi che sono sottoclassi di MessageTmpl, devo implementare il costruttore. In caso contrario, non verrà registrato in MessageFactory.

    Ad esempio commentando il costruttore del PongMessage:

      class PongMessage: public MessageTmpl  { public: //PongMessage() {} /* HERE */ virtual void say() { printf("Pong\n"); } }; 

    risulterebbe nella class PongMessage non registrata da MessageFactory e il programma sarebbe segmentato nella riga MessageFactory :: Create (11) . La domanda è
    perché la class non si registra? Dovendo aggiungere l’implementazione vuota dei 100+ messaggi di cui ho bisogno mi sento inefficiente e inutile.

Risposta Uno

La tecnica generale di derivare una class come questa è il modello di modello Curiosamente ricorrente (CRTP) :

 class PingMessage: public MessageTmpl < 10, PingMessage > 

La tua tecnica specifica di utilizzare l’inizializzazione dei membri statici di una class template per registrare sottoclassi di quella class è (IMO) semplicemente geniale, e non l’ho mai visto prima. Un approccio più comune, utilizzato da framework di test unitari come UnitTest ++ e Google Test , consiste nel fornire macro che dichiarino sia una class sia una variabile statica separata inizializzando quella class.

Risposta Due

Le variabili statiche sono inizializzate nell’ordine elencato. Se sposti la tua dichiarazione m_List prima delle tue chiamate a MessageFactory :: Register, dovresti essere al sicuro. Inoltre, tieni presente che se inizi a dichiarare sottoclassi di messaggi in più di un file, dovrai avvolgere m_List come un singleton e controllare che sia inizializzato prima di ogni utilizzo, a causa del fiasco dell’ordine di inizializzazione statica di C ++ .

Risposta tre

I compilatori C ++ realizzeranno solo i membri del modello che vengono effettivamente utilizzati. I membri statici delle classi template non sono un’area del C ++ che ho usato molto, quindi potrei sbagliarmi qui, ma sembra che fornire il costruttore sia sufficiente per far sì che il compilatore pensi che MESSAGE_ID sia usato (assicurando così che MessageFactory: Il registro è chiamato).

Questo mi sembra molto intuitivo, quindi potrebbe trattarsi di un bug del compilatore. (Stavo testando questo in g ++ 4.3.2, sono curioso di sapere come Comeau C ++, ad esempio, lo gestisce.)

Anche l’istanziazione esplicita di MESSAGE_ID è sufficiente, almeno in g ++ 4.3.2:

 template const uint16_t PingMessage::MESSAGE_ID; 

Ma è un lavoro ancora più inutile che fornire un costruttore predefinito vuoto.

Non riesco a pensare ad una buona soluzione usando il tuo approccio attuale; Personalmente sarei tentato di passare a una tecnica (come le macro o l’utilizzo di uno script per generare parte dei file sorgente) che dipendeva meno dal C ++ avanzato. (Uno script avrebbe il vantaggio aggiuntivo di facilitare la manutenzione di MESSAGE_IDs.)

In risposta ai tuoi commenti:

I singleton sono generalmente da evitare perché sono spesso abusati come variabili globali scarsamente mascherate. Ci sono alcune volte, tuttavia, quando hai davvero bisogno di una variabile globale, e un registro globale delle sottoclassi Message disponibili è una di quelle volte.

Sì, il codice che hai fornito sta inizializzando MESSAGE_ID, ma stavo parlando di istanziare esplicitamente l’istanza di ogni sottoclass di MESSAGE_ID. L’istanziazione esplicita si riferisce all’istruire il compilatore per creare un’istanza di un modello anche se pensa che l’istanza del modello non sarà altrimenti utilizzata.

Sospetto che la funzione statica con l’assegnazione volatile sia lì per ingannare o forzare il compilatore a generare l’assegnazione MESSAGE_ID (per aggirare i problemi che dash-tom-bang e io ho fatto notare con il compilatore o il linker che lascia cadere o non istanzia l’assegnazione ).

Questa è una versione modificata che utilizza un singleton MessageFactory e una std :: map per memorizzare i costruttori. Funziona benissimo finora ma i commenti sono ben accetti.

Sto ancora cercando di trovare un modo per evitare di creare costruttori per ogni class di messaggi. So che è ansible perché la libreria originale può farlo. Purtroppo ho solo i file di intestazione quindi nessuna idea sui dettagli di implementazione.

 #include  #include  #include  #include  #include  class Message { protected: Message() {}; public: virtual ~Message() { } int getMessageType() const { return m_type; } virtual void say() = 0; protected: uint16_t m_type; }; template class MessageTmpl: public Message { enum { _MESSAGE_ID = TYPE }; public: static Message* Create() { return new IMPL(); } static const uint16_t MESSAGE_ID; // for registration static void Enable() { volatile uint16_t x = MESSAGE_ID; } protected: MessageTmpl() { m_type = MESSAGE_ID; } //use parameter to instanciate template }; class MessageFactory { public: typedef Message* (*t_pfFactory)(); static MessageFactory *getInstance() { static MessageFactory fact; return &fact; } uint16_t Register(uint16_t msgid, t_pfFactory factoryMethod) { printf("Registering constructor for msg id %d\n", msgid); m_List[msgid] = factoryMethod; return msgid; } Message *Create(uint16_t msgid) { return m_List[msgid](); } std::map m_List; private: MessageFactory() {}; MessageFactory(MessageFactory const&) {}; MessageFactory& operator=(MessageFactory const&); ~MessageFactory() {}; }; //std::map MessageFactory::m_List; template  const uint16_t MessageTmpl::MESSAGE_ID = MessageFactory::getInstance()->Register( MessageTmpl::_MESSAGE_ID, &MessageTmpl::Create); class PingMessage: public MessageTmpl < 10, PingMessage > { public: PingMessage() {} virtual void say() { printf("Ping\n"); } }; class PongMessage: public MessageTmpl < 11, PongMessage > { public: PongMessage() {} virtual void say() { printf("Pong\n"); } }; int main(int argc, char **argv) { Message *msg1; Message *msg2; msg1 = MessageFactory::getInstance()->Create(10); msg1->say(); msg2 = MessageFactory::getInstance()->Create(11); msg2->say(); delete msg1; delete msg2; return 0; } 

Penso che tu stia imbattendo in un comportamento non specificato perché le tue registrazioni possono verificarsi prima che l’object in cui vuoi inserirle. Potresti ottenere risultati ok perché lo spazio dell’array è incorporato nello stack principale del programma. Chissà…

La correzione per questo che ho usato è di rendere la funzione di registrazione esterna o una funzione membro piuttosto che statica. Quindi usa un singleton Meyers:

 MessageFactory * MessageFactory::instance() { static MessageFactory fact; return &fact; } 

In questo modo la tua fabbrica di messaggi verrà creata al momento dell’accesso da qualsiasi altra cosa e sarà garantita la disponibilità quando tenterai di usarla (perché provare a usarla la prima volta che la crea).

Sono stato in grado di far funzionare il codice Horacio senza utilizzare i costruttori nelle classi derivate. Ho chiamato la funzione enable all’interno della funzione say delle classi derivate.

 class PingMessage: public MessageTmpl < 10, PingMessage > { public: //PingMessage () {} virtual void say () { enable (); // virtual (not static) function of the template class printf ("Ping\n"); } }; 

2: potresti usare un contenitore dinamico, ma dovresti anche cambiare la modalità di registrazione ecc. Ad esempio, potresti usare una mappa con un int come chiave e un puntatore a funzione come elemento:

 typedef Message* ( *NewMessageFun )(); template< class tMessage > Message* NewMessage() { return new tMessage(); }; class PingMessage : public MessageImpl { public: enum{ _MESSAGE_ID = 10 }; }; class PongMessage { public: enum{ _MESSAGE_ID = 11 }; } //factory std::map< int, NewMessageFun > mymap; bool Register( const int type, NewMessageFun fun ) { if( mymap.contains( type ) ) return false; //already registered! mymap[ type ] = fun; return true; } template< class tMessage > bool RegisterAny() //shortcut { return Register( tMessage::_MESSAGE_ID, NewMessage< tMessage > ); } // //main factory.RegisterAny< PingMessage >(); factory.RegisterAny< PongMessage >(); // 

Oppure, nel codice corrente, basta utilizzare una dimensione di allocazione ragionevole e controllare i limiti di runtime per vedere che ci sono troppe registrazioni. E magari fornire un metodo ‘Annulla registrazione’.

ecco un elenco leggermente modificato usando la mappa

 #include  #include  #include  #include  #include  #include  //typedef Message *; class MessageFactory { public: struct register_base { virtual int id() const = 0; virtual Message* new_() = 0; }; template struct register_ : register_base { static const int ID; register_() : id_(ID) {} // force ID initialization int id() const { return C::factory_key; } Message* new_() { return new C(); } private: const int id_; }; static uint16_t Register(register_base* message) { printf("Registering constructor for msg id %d\n", message->id()); m_List[message->id()] = message; return message->id(); } static Message *Create(uint16_t msgid) { return m_List[msgid]->new_(); } static std::map m_List; }; std::map MessageFactory::m_List; template const int MessageFactory::register_::ID = MessageFactory::Register(new MessageFactory::register_()); class Message { public: virtual ~Message() {} int getMessageType() const { return m_type; } virtual void say() = 0; protected: uint16_t m_type; }; class PingMessage: public Message, private MessageFactory::register_ { public: static const int factory_key = 10; PingMessage() { } // must call explicitly to register virtual void say() { printf("Ping\n"); } }; class PongMessage:public Message, private MessageFactory::register_ { public: static const int factory_key = 11; PongMessage() { } void say() { printf("Pong\n"); std::cout << this->id() << std::endl; } }; int main(int argc, char **argv) { Message *msg1; Message *msg2; msg1 = MessageFactory::Create(10); msg1->say(); msg2 = MessageFactory::Create(11); msg2->say(); delete msg1; } 

Per tutte le classi di messaggi che sono sottoclassi di MessageTmpl, devo implementare il costruttore. In caso contrario, non verrà registrato in MessageFactory.

Stavo sperimentando questa idea e ho trovato un modo per forzare l’istanziazione della variabile di registrazione senza dover fare nulla nella class derivata. Crea una funzione virtuale nel modello che accede alla variabile di registrazione. Ciò impone l’istanziazione della funzione perché la funzione virtuale deve essere presente indipendentemente dal fatto che venga mai chiamata.

Ecco il mio codice digitale:

 #include  #include  struct thingy { virtual ~thingy() {} virtual void print_msg() const = 0; virtual size_t id() const = 0; bool registered_already() const { return registered; } protected: bool registered; }; struct holder { enum index { ID_OPEN , ID_SAVE , ID_SAVEAS , COUNT }; static holder& instance() { static holder inst; return inst; } thingy& operator[] (size_t i) { assert(thingys[i] && "Not registered."); return *thingys[i]; } bool registered(size_t i) const { return thingys[i] != 0; } ~holder() { std::for_each(thingys.begin(), thingys.end(), [](thingy* t) { delete t; }); } index reg(thingy* t, index i) { assert( !thingys[i] && "Thingy registered at this ID already" ); thingys[i] = t; return i; } private: holder() : thingys() {} boost::array< thingy*, COUNT > thingys; }; template < typename Derived, holder::index i > struct registered_thingy : thingy { size_t id() const { return registration; } private: static holder::index registration; }; template < typename T, holder::index i > holder::index registered_thingy::registration = holder::instance().reg(new T, i); struct thingy1 : registered_thingy { void print_msg() const { std::cout << "thingy1\n"; } }; struct thingy2 : registered_thingy { void print_msg() const { std::cout << "thingy2\n"; } }; struct thingy3 : registered_thingy { void print_msg() const { std::cout << "thingy3\n"; } }; int main() { holder::instance()[holder::ID_OPEN].print_msg(); std::cin.get(); }