2010-05-17 17 views
11

Al implementar una clase MessageFactory a instatiate Los objetos de mensaje que he usado algo como:registran dinámicamente los métodos constructores en un AbstractFactory en tiempo de compilación usando C++ plantillas

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

Esto funciona bien, pero cada vez que añada un nuevo mensaje que tengo para agregar un nuevo XXX_MSG y modificar la declaración de cambio.

Después de algunas investigaciones, encontré una forma de actualizar dinámicamente el MessageFactory en tiempo de compilación para que pueda agregar tantos mensajes como quiera sin necesidad de modificar el MessageFactory. Esto permite una más limpio y más fácil de mantener el código, ya que no necesito modificar tres lugares diferentes para añadir/quitar clases de mensajes:

#include <stdio.h>                                           
#include <stdlib.h>                                           
#include <string.h>                                           
#include <inttypes.h>                                           

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<int TYPE, typename IMPL>                                        
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 <int TYPE, typename IMPL>                                       
const uint16_t MessageTmpl<TYPE, IMPL >::MESSAGE_ID = MessageFactory::Register(                            
    MessageTmpl<TYPE, IMPL >::_MESSAGE_ID, &MessageTmpl<TYPE, IMPL >::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"); }                                     
};                                               

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;                                             
} 

la plantilla aquí ¿La magia mediante el registro en la clase MessageFactory, todas las nuevas clases de mensajes (por ejemplo, PingMessage y PongMessage) esa subclase de MessageTmpl.

Esto funciona muy bien y simplifica el mantenimiento del código, pero todavía tengo algunas preguntas acerca de esta técnica:

  1. Es ésta una técnica/patrón conocido? ¿cual es el nombre? Quiero buscar más información al respecto.

  2. Quiero hacer la matriz para almacenar los nuevos constructores MessageFactory :: m_list [65536] un std :: mapa, pero al hacerlo hace que el programa segfault incluso antes de llegar a main(). Crear una matriz de 65536 elementos es exagerado, pero no he encontrado una forma de hacer de este un contenedor dinámico.

  3. Para todas las clases de mensajes que son subclases de MessageTmpl tengo que implementar el constructor. Si no, no se registrará en MessageFactory.

    Por ejemplo comentando el constructor de la PongMessage:

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

    daría lugar a la clase PongMessage no estar registrado por el MessageFactory y el programa habría segfault en el MessageFactory :: Create (11) línea . La pregunta es
    ¿por qué la clase no se registrará? Tener que agregar la implementación vacía de los 100+ mensajes que necesito se siente ineficaz e innecesaria.

+0

# 1 es CRTP (tipo de) http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern – Anycorn

+0

# 3, ya que el constructor de MessageTmpl está protegido (tal vez) – Anycorn

+0

dicho sea de paso, consulte los horarios de código. se ha extraviado; y. caracteres. Lo compilé, sin embargo, tengo un error de segmentación al ejecutarlo. – Anycorn

Respuesta

7

respuesta Una

La técnica general de derivar una clase como esta es la Curiously Recurring Template Pattern (CRTP):

class PingMessage: public MessageTmpl < 10, PingMessage > 

Su técnica específica de la utilización de inicialización miembro estático de una clase de plantilla para registrar las subclases de dicha clase es (IMO) simplemente brillante, y nunca he visto eso antes. Un enfoque más común, utilizado por los marcos de prueba de unidades como UnitTest++ y Google Test, es proporcionar macros que declaran una clase y una variable estática separada inicializando esa clase.

respuesta Dos

Las variables estáticas son inicializados en el orden indicado. Si mueve su declaración m_List antes de sus llamadas a MessageFactory :: Register, debe estar seguro. También tenga en cuenta que si comienza a declarar subclases de mensajes en más de un archivo, deberá ajustar m_List como singleton y comprobar que se haya inicializado antes de cada uso, debido al C++ static initialization order fiasco.

compiladores respuesta Tres

C++ sólo una instancia de miembros de la plantilla que se utilizan en realidad. Los miembros estáticos de las clases de plantilla no son un área de C++ que he usado mucho, así que podría estar equivocado aquí, pero parece que proporcionar el constructor es suficiente para hacer que el compilador piense que se usa MESSAGE_ID (asegurando así que MessageFactory :: Registrarse es llamado).

Esto me parece muy poco intuitivo, por lo que puede ser un error del compilador. (Que estaba probando esto en g ++ 4.3.2; Tengo curiosidad por saber cómo Comeau C++, por ejemplo, se ocupa de él.)

explícitamente instanciar MESSAGE_ID también es suficiente, al menos en g ++ 4.3.2:

template const uint16_t PingMessage::MESSAGE_ID; 

Pero eso es incluso más trabajo innecesario que proporcionar un constructor predeterminado vacío.

No puedo pensar en una buena solución utilizando su enfoque actual; Personalmente estaría tentado de cambiar a una técnica (como macros o usar un script para generar parte de tus archivos fuente) que dependía menos de C++ avanzado. (Un script tendría la ventaja adicional de facilitar el mantenimiento de MESSAGE_IDs.)

En respuesta a sus comentarios:

Singleton son generalmente que hay que evitar, porque a menudo son usados ​​en exceso variables globales como mal disimulada . Sin embargo, hay algunas veces en las que realmente se necesita una variable global, y un registro global de subclases de mensajes disponibles es uno de esos momentos.

Sí, el código que proporcionó está inicializando MESSAGE_ID, pero estaba hablando de explicitly instantiating cada instancia de subclase de MESSAGE_ID. La creación de instancias explícita se refiere a instruir al compilador para crear una instancia de una plantilla, incluso si cree que esa instancia de plantilla no se utilizará de otro modo.

Sospecho que la función estática con la asignación volátil está ahí para engañar o forzar al compilador a generar la asignación MESSAGE_ID (para evitar los problemas que dash-tom-bang y yo señalamos con el compilador o el enlazador cayendo o no instanciar la tarea).

+0

El OP también debe tener en cuenta que muchos vinculadores eliminan objetos no referenciados automáticamente, incluso si realizan un trabajo interesante (es decir, la inicialización de los miembros estáticos desencadena el registro en una lista maestra). Visual Studio, por ejemplo, eliminará felizmente la inicialización de miembros estáticos en bibliotecas (es decir, código que no es parte de su proyecto "ejecutable") si el ejecutable no hace referencia explícitamente a ese objeto estático de alguna manera. Puede agregar cada uno a una lista de "objetos referenciados implícitamente" en VS, pero eso es aún más una IMO molesta. –

+0

Gracias por sus respuestas. Aprendí mucho de ellos. No tomo ningún crédito de este código ya que lo copié de una biblioteca de redes japonesa llamada nueve. Estoy buscando ahora algo único pero entiendo que deben evitarse. Creo que la plantilla que registra los constructores también está inicializando explícitamente el MESSAGE_ID? puedes ver un signo igual allí. Finalmente, la implementación original tiene este código en la plantilla MessageTmpl: static void Enable() {volatile uint16_t x = MESSAGE_ID; } Pero no tengo idea de cómo se usa para habilitar el registro del constructor. – Horacio

+1

El vacío estático Enable() {volatile uint16_t x = MESSAGE_ID; } parece que está destinado a evitar que la optimización del enlazador penetre los objetos. Debido a que x es volátil, la tarea no puede optimizarse. –

0

2: se puede utilizar un contenedor dinámico, pero entonces también había tenido que cambiar la forma en la inscripción, etc. Por ejemplo, se podría utilizar un mapa con un int como clave y un puntero de función como elemento de:

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>(); 
// 

O, en su código actual, sólo tiene que utilizar un tamaño de asignación sensata y tienen límites de tiempo de ejecución de comprobación para ver que hay demasiados registros. Y tal vez suministre un método 'Anular el registro'.

+0

Sí, probé un mapa pero segfaulted ... pero gracias a las respuestas de Josh Kelley y Noah Roberts encontré el problema (orden de inicialización estático). Supongo que tendré que irme a Singleton aquí, pero escuché que son malvados. – Horacio

+1

los singletons no son siempre malvados. Si crees, para tu caso, un singleton es el mejor enfoque, entonces lo es. – stijn

2

Creo que se está encontrando con un comportamiento no especificado porque sus registros pueden ocurrir antes que el objeto al que desea pegarlos. Puede estar obteniendo buenos resultados porque el espacio de la matriz está integrado en la pila principal del programa. Quién sabe ...

La solución para esto que he usado es hacer que la función de registro sea externa o una función miembro en lugar de estática. A continuación, utilice un producto único Meyers:


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

De esta manera su fábrica de mensaje se creó en el acceso de cualquier otra cosa y se garantiza que esté disponible cuando intenta utilizarla (porque tratar de usarlo la primera vez que lo crea)

0

aquí se modifica ligeramente la lista en el mapa

#include <map> 
#include <iostream> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <inttypes.h> 

//typedef Message *; 
class MessageFactory { 
public: 
    struct register_base { 
     virtual int id() const = 0; 
     virtual Message* new_() = 0; 
    }; 
    template<class C> 
    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<int,register_base*> m_List; 
}; 
std::map<int, MessageFactory::register_base*> MessageFactory::m_List; 

template<class C> 
const int MessageFactory::register_<C>::ID = 
    MessageFactory::Register(new MessageFactory::register_<C>()); 


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_<PingMessage> { 
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_<PongMessage> { 
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; 
} 
+0

Gracias, solucioné este problema moviendo la declaración std :: map antes del código de registro de MessageTmpl mencionado por Josh Kelley. Ahora puedo usar un std :: map en lugar de una matriz fija. Todavía tengo que hacer esto como Singleton para evitar problemas a medida que aumenta el número de mensajes. – Horacio

3

Ésta es una versión modificada que utiliza un conjunto unitario MessageFactory y un std :: mapa para constructores de tiendas. Funciona muy bien hasta el momento, pero los comentarios son bienvenidos.

Todavía estoy tratando de encontrar una manera de evitar crear constructores para cada clase de mensaje. Sé que es posible porque la biblioteca original puede hacerlo. Lamentablemente, solo tengo los archivos de encabezado, así que no tengo idea de los detalles de implementación.

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <inttypes.h> 
#include <map> 

class Message 
{ 
    protected: 
     Message() {}; 

    public: 
     virtual ~Message() { } 
     int getMessageType() const { return m_type; } 
     virtual void say() = 0; 

    protected: 
     uint16_t m_type; 
}; 

template<int TYPE, typename IMPL> 
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<uint16_t, t_pfFactory> m_List; 

    private: 
    MessageFactory() {}; 
    MessageFactory(MessageFactory const&) {}; 
    MessageFactory& operator=(MessageFactory const&); 
    ~MessageFactory() {}; 
}; 

//std::map<uint16_t, t_pfFactory> MessageFactory::m_List; 

template <int TYPE, typename IMPL> 
const uint16_t MessageTmpl<TYPE, IMPL>::MESSAGE_ID = MessageFactory::getInstance()->Register(
    MessageTmpl<TYPE, IMPL >::_MESSAGE_ID, &MessageTmpl<TYPE, IMPL >::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; 
} 
0

Para todas las clases de mensajes que son subclases de MessageTmpl que tengo que aplicar el constructor. Si no, no se registrará en MessageFactory.

Estaba experimentando con esta idea y se me ocurrió una forma de forzar la creación de instancias de la variable de registro sin tener que hacer nada en la clase derivada. Realice una función virtual en la plantilla que accede a la variable de registro. Esto obliga a la función a ser instanciada porque la función virtual debe estar allí, ya sea que se llame o no.

Aquí está mi código cero:

#include <boost/array.hpp> 
#include <iostream> 

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<T,i>::registration = holder::instance().reg(new T, i); 

struct thingy1 : registered_thingy<thingy1,holder::ID_OPEN> 
{ 
    void print_msg() const { std::cout << "thingy1\n"; } 
}; 

struct thingy2 : registered_thingy<thingy2, holder::ID_SAVE> 
{ 
    void print_msg() const { std::cout << "thingy2\n"; } 
}; 

struct thingy3 : registered_thingy<thingy3, holder::ID_SAVEAS> 
{ 
    void print_msg() const { std::cout << "thingy3\n"; } 
}; 



int main() 
{ 
    holder::instance()[holder::ID_OPEN].print_msg(); 

    std::cin.get(); 
} 
+0

Lamento decir que no puedo hacer que este código funcione. Obtengo la afirmación No registrado, lo que significa que los thingys no están siendo registrados. De nuevo, si proporciono los constructores de thingys, el código funciona bien. – Horacio

+0

¿Qué compilador estás usando? Funciona para mí y, por lo que puedo decir, debería. Creo que el compilador se ve obligado a crear una instancia de id(), que usa la variable ... –

+0

Maldita sea. 14.7.1/9: "... No se especifica si una implementación ejemplifica implícitamente una función miembro virtual de una plantilla de clase si la función miembro virtual no se instanciaría de otra manera". –

1

que era capaz de hacer que el código Horacio trabajar sin necesidad de utilizar los constructores de las clases derivadas. Llamé a la función enable dentro de la función say de las clases derivadas.

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