问题 使用C ++模板在编译时在AbstractFactory中动态注册构造函数方法


当实现MessageFactory类来实例化Message对象时,我使用了类似的东西:

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

这工作正常但每次添加新消息时我都要添加一个新的XXX_MSG并修改switch语句。

经过一些研究后,我发现了一种在编译时动态更新MessageFactory的方法,因此我可以根据需要添加任意数量的消息,而无需修改MessageFactory本身。这样可以更简洁,更容易维护代码,因为我不需要修改三个不同的位置来添加/删除消息类:

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

这里的模板通过在MessageFactory类中注册从MessageTmpl继承的所有新Message类(例如PingMessage和PongMessage)来实现神奇。

这很好用并简化了代码维护,但我仍然对这个技术有一些疑问:

  1. 这是一种已知的技术/模式吗?是什么名字?我想搜索更多信息 关于它。

  2. 我想创建用于存储新构造函数的数组 的MessageFactory :: m_List [65536]  一个std :: map但这样做会导致程序在到达main()之前发生段错误。 创建一个包含65536个元素的数组是有点过分,但我还没有找到方法 使这个动态容器。

  3. 对于作为MessageTmpl子类的所有消息类,我必须实现 构造函数。如果不是,它将不会在MessageFactory中注册。

    例如,注释PongMessage的构造函数:

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

    会导致PongMessage类没有被MessageFactory和 程序会在 的MessageFactory ::创建(11) 线。问题是
    为什么班级不会注册?必须添加100+的空实现 我需要的信息效率低下且不必要。


10475
2018-05-17 15:09


起源

#1是CRTP(种类) en.wikipedia.org/wiki/Curiously_recurring_template_pattern - Anycorn
#3因为MessageTmpl的构造函数受到保护(可能) - Anycorn
顺便说一句,检查你的代码清单。它有流浪;和。字符。我编译了它,但是运行它会出现分段错误。 - Anycorn
+1用于真正很酷的类模板静态成员初始化。 - Josh Kelley
我可以在使用gcc 4.4.3的Ubuntu 10.04中编译和运行此代码。 g ++ -g -Wall -o message message.cpp可以指出流浪;和。代码中的字符? - Horacio


答案:


答案一

派生这样的类的一般技术是 奇怪的重复模板模式(CRTP)

class PingMessage: public MessageTmpl < 10, PingMessage > 

使用模板类的静态成员初始化来注册该类的子类的特定技术(IMO)简直太棒了,我以前从未见过。一种更常见的方法,由单元测试框架使用 单元测试++ 和 谷歌测试,是提供声明类和初始化该类的单独静态变量的宏。

答案二

静态变量按列出的顺序初始化。如果在MessageFactory :: Register调用之前移动m_List声明,则应该是安全的。还要记住,如果你开始在多个文件中声明Message子类,你必须将m_List包装为一个单例并检查它是否在每次使用之前被初始化,因为 C ++静态初始化命令fiasco

答案三

C ++编译器只会实例化实际使用的模板成员。模板类的静态成员不是我用过的C ++领域,所以我在这里可能是错的,但看起来提供构造函数足以让编译器认为使用了MESSAGE_ID(从而确保MessageFactory ::注册被称为)。

这对我来说似乎非常不直观,所以它可能是一个编译器错误。 (我在g ++ 4.3.2中对此进行了测试;我很想知道Comeau C ++是如何处理它的。)

显式实例化MESSAGE_ID也足够了,至少在g ++ 4.3.2中:

template const uint16_t PingMessage::MESSAGE_ID;

但这比提供一个空的默认构造函数更加不必要。

使用您当前的方法我想不出一个好的解决方案;我个人很想转而使用一种技术(比如宏或使用脚本来生成部分源文件),这种技术较少依赖于高级C ++。 (脚本的另一个优点是可以简化MESSAGE_ID的维护。)

回应你的意见:

单身人士 通常 要避免,因为它们经常被过度使用,因为它们是伪装成全球变量的。但是,有几次,当你确实需要一个全局变量时,可用的Message子类的全局注册表就是其中之一。

是的,您提供的代码是初始化MESSAGE_ID,但我在谈论 明确地实例化 每个子类的MESSAGE_ID实例。显式实例化指的是指示编译器实例化模板,即使它认为不会使用该模板实例。

我怀疑具有volatile赋值的静态函数可以欺骗或强制编译器生成MESSAGE_ID赋值(以解决dash-tom-bang和我指出的编译器或链接器丢弃或不实例化赋值的问题)。


7
2018-05-17 16:29



OP还应该记住,许多链接器会自动删除未引用的对象,即使它们执行有趣的工作(即静态成员的初始化触发注册到主列表中)。例如,如果可执行文件没有以某种方式显式引用该静态对象,Visual Studio将很乐意丢弃库中的静态成员初始化(即,不属于“可执行”项目的代码)。你可以将每一个添加到VS中的“隐式引用对象”列表中,但这更像是一个麻烦的IMO。 - dash-tom-bang
谢谢你的回答。我从中学到了很多东西。我没有从这个代码中获得信任,因为我从一个名为nine的日本网络库中复制了它。我现在正在寻找单身人士的事情但我明白这些都应该避免。我虽然注册构造函数的模板也明确初始化了MESSAGE_ID?你可以在那里看到一个等号。最后,原始实现在MessageTmpl模板中具有以下代码:static void Enable(){volatile uint16_t x = MESSAGE_ID;但我不知道如何使用它来启用构造函数注册。 - Horacio
static void Enable(){volatile uint16_t x = MESSAGE_ID;看起来它旨在防止链接器优化剥离对象。由于x是易失性的,因此无法优化分配。 - Jaap Versteegh


这是一个修改版本,它使用MessageFactory单例和std :: map来存储构造函数。它到目前为止工作得很好,但欢迎提出意见。

我仍然试图找到一种方法来避免为每个消息类创建构造函数。我知道是可能的,因为原始库可以做到。不幸的是我只有头文件所以不知道实现细节。

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

4
2018-05-18 06:38





我认为您遇到了未指定的行为,因为您的注册可以在您想要粘贴它们的对象之前发生。您可能会得到正确的结果,因为数组的空间内置在程序的主堆栈中。谁知道...

我使用的修复方法是将注册函数设置为外部或成员函数而不是静态。然后使用迈耶斯单身人士:


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

这样,您的消息工厂将在其他任何内容访问时创建,并且当您尝试使用它时将保证可用(因为在第一次创建它时尝试使用它)。


2
2018-05-17 16:37





我能够在不使用派生类中的构造函数的情况下使Horacio代码工作。我在派生类的say函数中调用了enable函数。

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

1
2017-10-23 23:25





2:你可以使用动态容器,但是你还必须改变注册的方式等。例如,你可以使用带有int作为键的映射和作为元素的函数指针:

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

或者,在您当前的代码中,只需使用合理的分配大小并运行运行时边界检查以查看注册过多。并且可能提供“取消注册”方法。


0
2018-05-17 15:56



是的,我尝试了一张地图,但是它已经分段了......但是由于Josh Kelley和Noah Roberts的回答,我发现了问题(静态初始化顺序)。我猜我将不得不去辛格尔顿,但我听说这些都是邪恶的。 - Horacio
单身人士并不总是邪恶的。如果您认为,对于您的情况,单身是最好的方法,那么它就是。 - stijn


这里是使用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
2018-05-17 17:54



谢谢,我通过在Josh Kelley提到的MessageTmpl注册码之前移动std :: map声明来解决这个问题。现在我可以使用std :: map而不是固定数组。我还需要将其作为单身人士,以避免因消息编号增加而出现问题。 - Horacio


对于作为MessageTmpl子类的所有消息类,我必须实现构造函数。如果不是,它将不会在MessageFactory中注册。

我正在尝试这个想法,并提出了一种强制实例化注册变量的方法,而不必在派生类中做任何事情。在模板中创建一个访问注册变量的虚函数。这会强制函数被实例化,因为虚函数必须在那里,无论它是否被调用。

这是我的临时代码:

#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
2018-06-30 17:53



很抱歉,我无法使此代码生效。我得到了Not registered断言,这意味着thingys没有被注册。再次,如果我提供thingys构造函数,代码工作正常。 - Horacio
你用的是什么编译器?它适用于我,据我所知。我相信编译器被迫实例化id(),它使用变量... - Crazy Eddie
该死的。 14.7.1 / 9:“......如果虚拟成员函数不会被实例化,那么实现是否隐式实例化类模板的虚拟成员函数是未指定的。” - Crazy Eddie
您可能会尝试在main中添加对id()的调用。这应该会迫使这个问题。 - Crazy Eddie