Dynamically register constructor methods in an AbstractFactory at compile time using C++ templates

后端 未结 7 2031
予麋鹿
予麋鹿 2020-12-02 19:19

When implementing a MessageFactory class to instatiate Message objects I used something like:

class MessageFactory 
{
  public:
    static Message *create(in         


        
相关标签:
7条回答
  • 2020-12-02 20:04

    2: you could use a dynamic container, but then you'd also had to change how the registration etc. For example, you could use a map with an int as key and a function pointer as element:

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

    Or, in your current code, just use a sensible allocation size and have runtime bounds checking to see there are too much registrations. And maybe supply an 'Unregister' method.

    0 讨论(0)
  • 2020-12-02 20:06

    I think that you're running into unspecified behavior because your registrations can occur before the object you want to stick them into does. You may be getting ok results because the array's space is built into the program's main stack. Who knows...

    The fix for this that I've used is to make the registration function either external or a member function rather than static. Then use a Meyers singleton:

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

    This way your message factory will be created upon access by anything else and will be guaranteed to be available when you try to use it (because trying to use it the first time creates it).

    0 讨论(0)
  • 2020-12-02 20:06

    I was able to make the Horacio code working without using the constructors in the derived classes. I called the enable function inside the say function of the derived classes.

    class PingMessage: public MessageTmpl < 10, PingMessage >
    { 
      public:
      //PingMessage () {}
      virtual void say ()
      {
        enable ();  // virtual (not static) function of the template class
        printf ("Ping\n");
      }
    };
    
    0 讨论(0)
  • 2020-12-02 20:07

    here is slightly modified listing using map

    #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 讨论(0)
  • 2020-12-02 20:19

    Answer One

    The general technique of deriving a class like this is the Curiously Recurring Template Pattern (CRTP):

    class PingMessage: public MessageTmpl < 10, PingMessage > 
    

    Your specific technique of using a template class's static member initialization to register subclasses of that class is (IMO) simply brilliant, and I've never seen that before. A more common approach, used by unit test frameworks like UnitTest++ and Google Test, is to provide macros that declare both a class and a separate static variable initializing that class.

    Answer Two

    Static variables are initialized in the order listed. If you move your m_List declaration before your MessageFactory::Register calls, you should be safe. Also keep in mind that if you start declaring Message subclasses in more than one file, you'll have to wrap m_List as a singleton and check that it's initialized before each use, due to the C++ static initialization order fiasco.

    Answer Three

    C++ compilers will only instantiate template members that are actually used. Static members of template classes is not an area of C++ that I've used much, so I could be wrong here, but it looks like providing the constructor is enough to make the compiler think that MESSAGE_ID is used (thus ensuring that MessageFactory::Register is called).

    This seems very unintuitive to me, so it may be a compiler bug. (I was testing this in g++ 4.3.2; I'm curious to know how Comeau C++, for example, handles it.)

    Explicitly instantiating MESSAGE_ID also suffices, at least in g++ 4.3.2:

    template const uint16_t PingMessage::MESSAGE_ID;
    

    But that's even more unnecessary work than providing an empty default constructor.

    I can't think of a good solution using your current approach; I'd personally be tempted to switch to a technique (such as macros or using a script to generate part of your source files) that relied less on advanced C++. (A script would have the added advantage of easing maintenance of MESSAGE_IDs.)

    In response to your comments:

    Singletons are generally to be avoided because they're often overused as poorly disguised global variables. There are a few times, however, when you really do need a global variable, and a global registry of available Message subclasses is one of those times.

    Yes, the code that you provided is initializing MESSAGE_ID, but I was talking about explicitly instantiating each subclass's instance of MESSAGE_ID. Explicit instantiation refers to instructing the compiler to instantiate a template even if it thinks that that template instance won't otherwise be used.

    I suspect that the static function with the volatile assignment is there to trick or force the compiler into generating the MESSAGE_ID assignment (to get around the problems that dash-tom-bang and I pointed out with the compiler or linker dropping or not instantiating the assignment).

    0 讨论(0)
  • 2020-12-02 20:19

    For all message classes that are subclasses of MessageTmpl I have to implement the constructor. If not it won't register in the MessageFactory.

    I was experimenting with this idea and came up with a way to force instantiation of the registration variable without having to make anything in the derived class. Make a virtual function in the template that accesses the registration variable. This forces the function to be instantiated because the virtual function must be there whether or not it is ever called.

    Here's my scratch code:

    #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 讨论(0)
提交回复
热议问题