2010-05-17 5 views
11

Lors de l'implémentation d'une classe MessageFactory à instatiate objets de message que j'ai utilisé quelque chose comme:registre Dynamiquement méthodes de constructeur Abstraite au moment de la compilation en utilisant des modèles C++

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

Cela fonctionne ok, mais chaque fois que j'ajouter un nouveau message que je pour ajouter un nouveau XXX_MSG et modifier l'instruction switch. Après quelques recherches, j'ai trouvé un moyen de mettre à jour dynamiquement MessageFactory à la compilation afin que je puisse ajouter autant de messages que je veux sans avoir besoin de modifier le MessageFactory lui-même. Cela permet plus propre et plus facile à maintenir le code que je ne ai pas besoin de modifier trois endroits différents pour ajouter/supprimer des classes de message:

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

Le modèle fait ici la magie en vous inscrivant dans la classe MessageFactory, toutes les nouvelles classes de messages (par exemple PingMessage et PongMessage) cette sous-classe de MessageTmpl.

Cela fonctionne très bien et simplifie la maintenance du code mais j'ai encore quelques questions au sujet de cette technique:

  1. Est-ce une technique connue/modèle? quel est le nom? Je veux rechercher plus d'informations à ce sujet.

  2. Je veux faire le tableau pour stocker de nouveaux constructeurs MessageFactory :: m_List [65536] std :: carte, mais cela pourrait provoquer des programme à atteindre avant même segmentation fault main(). Créer un tableau de 65536 éléments est exagéré, mais je n'ai pas trouvé un moyen de en faire un conteneur dynamique. Pour toutes les classes de message qui sont des sous-classes de MessageTmpl, je dois implémenter le constructeur . Sinon, il ne sera pas enregistré dans la MessageFactory.

    Par exemple commentant le constructeur de la PongMessage:

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

    entraînerait la classe PongMessage ne pas être enregistrée par le MessageFactory et le programme aurait segfault dans le MessageFactory :: Créer (11) ligne . La question est
    pourquoi la classe ne s'enregistrera pas? Devoir ajouter l'implémentation vide des 100+ messages dont j'ai besoin me semble inefficace et inutile.

+0

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

+0

# 3 parce que constructeur de MessageTmpl est protégé (peut-être) – Anycorn

+0

par la manière, vérifiez votre liste de code. il a erré; et . personnages. Je l'ai compilé, mais je reçois une erreur de segmentation. – Anycorn

Répondre

7

Réponse Un

La technique générale de dériver une classe comme celui-ci est le Curiously Recurring Template Pattern (CRTP):

class PingMessage: public MessageTmpl < 10, PingMessage > 

Votre technique spécifique d'utiliser l'initialisation du membre statique d'une classe de modèle pour enregistrer les sous-classes de cette classe est (OMI) tout simplement génial, et je n'ai jamais vu ça auparavant. Une approche plus courante, utilisée par les frameworks de tests unitaires tels que UnitTest++ et Google Test, est de fournir des macros qui déclarent à la fois une classe et une variable statique séparée initialisant cette classe.

Réponse Deux

Les variables statiques sont initialisés dans l'ordre indiqué. Si vous déplacez votre déclaration m_List avant vos appels MessageFactory :: Register, vous devriez être en sécurité. Gardez également à l'esprit que si vous commencez à déclarer des sous-classes Message dans plusieurs fichiers, vous devrez envelopper m_List en tant que singleton et vérifier qu'il est initialisé avant chaque utilisation, en raison du C++ static initialization order fiasco.

Réponse Trois

C++ compilateurs seront uniquement instancier membres de modèles qui sont réellement utilisés. Les membres statiques des classes template ne sont pas une zone C++ que j'ai beaucoup utilisée, donc je peux me tromper ici, mais il semble que fournir le constructeur soit suffisant pour que le compilateur pense que MESSAGE_ID est utilisé (assurant ainsi que MessageFactory :: Le registre est appelé).

Cela semble très peu intuitif pour moi, donc ce peut être un bug de compilateur. (Je testais ce en g ++ 4.3.2, je suis curieux de savoir comment Comeau C++, par exemple, gère.)

instanciation Explicitement MESSAGE_ID suffit aussi, au moins en g ++ 4.3.2:

template const uint16_t PingMessage::MESSAGE_ID; 

Mais c'est un travail encore plus inutile que de fournir un constructeur par défaut vide.

Je ne peux pas penser à une bonne solution en utilisant votre approche actuelle; Personnellement, je serais tenté de passer à une technique (comme les macros ou l'utilisation d'un script pour générer une partie de vos fichiers sources) qui reposait moins sur le C++ avancé. (Un script aurait l'avantage de faciliter l'entretien des MESSAGE_IDs.)

En réponse à vos commentaires:

singletons sont généralement à éviter car ils sont souvent galvaudé comme des variables globales mal déguisées . Cependant, il y a quelques fois où vous avez vraiment besoin d'une variable globale, et un registre global des sous-classes Message disponibles est l'un de ces moments.

Oui, le code que vous avez fourni initialise MESSAGE_ID, mais je parlais de explicitly instantiating l'instance de MESSAGE_ID de chaque sous-classe. L'instanciation explicite fait référence à l'instruction du compilateur d'instancier un modèle même s'il pense que cette instance de modèle ne sera pas utilisée autrement.Je soupçonne que la fonction statique avec l'assignation volatile est là pour tromper ou forcer le compilateur à générer l'assignation MESSAGE_ID (pour contourner les problèmes que dash-tom-bang et j'ai signalés avec le compilateur ou l'éditeur de liens abandonnant ou ne pas instancier l'assignation).

+0

L'OP doit également garder à l'esprit que de nombreux liens suppriment automatiquement les objets non référencés, même s'ils effectuent un travail intéressant (c'est-à-dire que l'initialisation des membres statiques déclenche l'enregistrement dans une liste principale). Visual Studio, par exemple, abandonnera volontiers l'initialisation statique des membres dans les bibliothèques (c'est-à-dire le code qui ne fait pas partie de votre projet "exécutable") si l'exécutable ne fait pas explicitement référence à cet objet statique. Vous pouvez ajouter chacun à une liste d '"objets implicitement référencés" dans VS, mais c'est encore plus un problème d'OMI. –

+0

Merci pour vos réponses. J'ai appris beaucoup d'eux. Je ne prends aucun crédit de ce code comme je l'ai copié à partir d'une bibliothèque réseau japonaise appelée neuf. Je regarde maintenant la chose singleton mais je comprends que ceux-ci doivent être évités. Je pense que le modèle qui enregistre les constructeurs initialise explicitement le MESSAGE_ID? vous pouvez voir un signe égal là-bas. Enfin l'implémentation d'origine a ce code dans le modèle MessageTmpl: static void Enable() {volatile uint16_t x = MESSAGE_ID; } Mais je n'ai aucune idée de comment cela est utilisé pour activer l'enregistrement du constructeur. – Horacio

+1

Le static void Enable() {volatile uint16_t x = MESSAGE_ID; } semble vouloir empêcher l'optimisation de l'éditeur de liens de supprimer les objets. Du fait que x est volatile, l'affectation ne peut pas être optimisée. –

0

2: vous pouvez utiliser un conteneur dynamique, mais vous auriez dû également changer la façon dont la etc. inscription Par exemple, vous pouvez utiliser une carte avec un int comme la clé et un pointeur de fonction comme élément:

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

Ou, dans votre code actuel, il suffit d'utiliser une taille d'allocation raisonnable et avoir des bornes d'exécution de vérification pour voir il y a trop inscriptions. Et peut-être fournir une méthode «Unregister».

+0

Oui j'ai essayé une carte mais elle segfaulted ... mais grâce aux réponses de Josh Kelley et de Noah Roberts j'ai trouvé le problème (ordre d'initialisation statique). Je suppose que je vais devoir aller Singleton ici, mais j'ai entendu dire que ce sont des maux. – Horacio

+1

singletons ne sont pas toujours mal. Si vous pensez, pour votre cas, qu'un singleton est la meilleure approche, alors c'est le cas. – stijn

2

Je pense que vous rencontrez un comportement non spécifié car vos enregistrements peuvent se produire avant que l'objet que vous souhaitez les coller ne le soit. Vous obtiendrez peut-être des résultats corrects, car l'espace du tableau est intégré dans la pile principale du programme. Qui sait ...

Le correctif pour cela que j'ai utilisé est de rendre la fonction d'enregistrement externe ou une fonction membre plutôt que statique. Ensuite, utilisez un singleton Meyers:


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

De cette façon, votre usine de message sera créé sur l'accès par toute autre chose et sera garanti d'être disponible lorsque vous essayez de l'utiliser (parce que d'essayer de l'utiliser la première fois crée).

0

ici est légèrement modifié à l'aide liste carte

#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

Merci, j'ai résolu ce problème en déplaçant la déclaration std :: map avant le code d'enregistrement MessageTmpl tel que mentionné par Josh Kelley. Maintenant, je peux utiliser un std :: map au lieu d'un tableau fixe. Encore j'ai besoin de faire un Singleton pour éviter les problèmes que le numéro de message augmente. – Horacio

3

Ceci est une version modifiée qui utilise un singleton MessageFactory et un std :: carte pour les constructeurs de magasins. Cela fonctionne très bien jusqu'à présent mais les commentaires sont les bienvenus.

J'essaie toujours de trouver un moyen d'éviter de créer des constructeurs pour chaque classe de message. Je sais que c'est possible parce que la bibliothèque originale peut le faire. Malheureusement, je n'ai que les fichiers d'en-tête donc aucune idée sur les détails de l'implémentation.

#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

Pour toutes les classes de messages qui sont sous-classes de MessageTmpl je dois mettre en œuvre le constructeur. Sinon, il ne sera pas enregistré dans la MessageFactory. J'ai expérimenté avec cette idée et ai trouvé un moyen de forcer l'instanciation de la variable d'enregistrement sans avoir à faire quoi que ce soit dans la classe dérivée. Créez une fonction virtuelle dans le modèle qui accède à la variable d'enregistrement. Cela force la fonction à être instanciée parce que la fonction virtuelle doit être là, qu'elle soit appelée ou non.

Voici mon code zéro:

#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

Désolé de dire que je ne peux pas faire fonctionner ce code. Je reçois l'assertion non enregistrée qui signifie que les thingys ne sont pas enregistrés. Encore une fois si je fournis les constructeurs de thingys, le code fonctionne bien. – Horacio

+0

Quel compilateur utilisez-vous? Cela fonctionne pour moi et autant que je peux le dire. Je crois que le compilateur est forcé d'instancier id(), qui utilise la variable ... –

+0

Damn. 14.7.1/9: "... Il n'est pas spécifié si une implémentation instancie implicitement une fonction membre virtuelle d'un modèle de classe si la fonction de membre virtuel ne serait pas instanciée." –

1

j'ai pu rendre le code Horacio travailler sans utiliser les constructeurs dans les classes dérivées. J'ai appelé la fonction enable à l'intérieur de la fonction say des classes dérivées.

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