Динамически регистрировать методы конструктора в 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, все новые классы сообщений (например, PingMessage и PongMessage), которые являются подклассом из MessageTmpl.
Это отлично работает и упрощает обслуживание кода, но у меня все еще есть некоторые вопросы об этой технике:
-
Это известный метод/образец? Как тебя зовут? Я хочу найти дополнительную информацию
об этом.
-
Я хочу создать массив для хранения новых конструкторов MessageFactory:: m_List [65536]
std:: map, но это приводит к тому, что программа выполняет segfault даже до достижения main().
Создание массива из 65536 элементов является излишним, но я не нашел способ
сделайте это динамическим контейнером.
-
Для всех классов сообщений, которые являются подклассами MessageTmpl, я должен реализовать
конструктор. Если это не будет зарегистрировано в MessageFactory.
Например, комментируя конструктор PongMessage:
class PongMessage: public MessageTmpl < 11, PongMessage >
{
public:
//PongMessage() {} /* HERE */
virtual void say() { printf("Pong\n"); }
};
приведет к тому, что класс PongMessage не будет зарегистрирован MessageFactory и
программа будет segfault в строке MessageFactory:: Create (11). Вопрос:
почему класс не будет регистрироваться? Чтобы добавить пустую реализацию 100+
сообщения, которые мне нужны, неэффективны и не нужны.
Ответы
Ответ 1
Ответ один
Общей методикой получения такого класса является Curiously Recurring Template Pattern (CRTP):
class PingMessage: public MessageTmpl < 10, PingMessage >
Ваша конкретная методика использования инициализации статического члена класса шаблона для регистрации подклассов этого класса (IMO) просто блестящая, и я никогда раньше этого не видел. Более общий подход, используемый unit test фреймами, такими как UnitTest ++ и Google Test, заключается в предоставлении макросов, объявляющих как класс, так и отдельную статическую переменную, инициализирующую этот класс.
Ответ Два
Статические переменные инициализируются в указанном порядке. Если вы переместите объявление m_List перед вызовом MessageFactory:: Register, вы должны быть в безопасности. Также имейте в виду, что если вы начнете объявлять подклассы Message в более чем одном файле, вам нужно будет обернуть m_List как одноэлементный и проверить, что он инициализирован перед каждым использованием из-за Фиксация порядка статической инициализации С++.
Ответить Три
Компиляторы С++ будут создавать экземпляры только тех элементов, которые фактически используются. Статические члены классов шаблонов не являются областью С++, которую я использовал много, поэтому я могу ошибаться здесь, но похоже, что предоставление конструктора достаточно, чтобы компилятор подумал, что используется MESSAGE_ID (таким образом, чтобы MessageFactory:: Регистр называется).
Это кажется мне очень неинтуитивным, так что это может быть ошибка компилятора. (Я тестировал это в g++ 4.3.2, мне любопытно узнать, как, например, справляется с Комо С++).
Явное создание экземпляра MESSAGE_ID также достаточно, по крайней мере, в g++ 4.3.2:
template const uint16_t PingMessage::MESSAGE_ID;
Но это еще более ненужная работа, чем предоставление пустого конструктора по умолчанию.
Я не могу думать о хорошем решении, используя ваш текущий подход; Я лично испытываю соблазн переключиться на технику (например, макросы или использовать script для генерации части ваших исходных файлов), которые меньше полагаются на продвинутый С++. (A script будет иметь дополнительное преимущество для облегчения обслуживания MESSAGE_IDs.)
В ответ на ваши комментарии:
Синглтоны обычно следует избегать, потому что их часто используют в качестве замаскированных глобальных переменных. Однако есть несколько раз, когда вам действительно нужна глобальная переменная, и глобальный реестр доступных подклассов Message является одним из тех случаев.
Да, код, который вы предоставили, инициализирует MESSAGE_ID, но я говорил о явным образом создавая экземпляр каждого экземпляра подкласса MESSAGE_ID. Явное создание экземпляра означает указание компилятору создать экземпляр шаблона, даже если он считает, что этот экземпляр шаблона иначе не будет использоваться.
Я подозреваю, что статическая функция с волатильным присваиванием заключается в том, чтобы обмануть или заставить компилятор генерировать назначение MESSAGE_ID (чтобы обойти проблемы, связанные с тишиной-тобой, и я указал, что при компиляторе или компоновщике выпадает или нет создание экземпляра задания).
Ответ 2
Это модифицированная версия, в которой используется Singleton 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;
}
Ответ 3
Я думаю, что вы столкнулись с неопределенным поведением, потому что ваши регистрации могут произойти до того, как объект, который вы хотите вставить в него. Вы можете получать хорошие результаты, потому что пространство массива встроено в основной стек программы. Кто знает...
Исправление для этого, которое я использовал, состоит в том, чтобы сделать регистрационную функцию либо внешней, либо функцией-членом, а не статичной. Затем используйте синглтон Meyers:
MessageFactory * MessageFactory::instance()
{
static MessageFactory fact;
return &fact;
}
Таким образом ваше сообщение factory будет создано при доступе к чему-либо еще и будет гарантировано быть доступным, когда вы попытаетесь его использовать (поскольку попытка его использования в первый раз создает его).
Ответ 4
Мне удалось сделать код Horacio без использования конструкторов в производных классах. Я вызвал функцию enable внутри функции say производных классов.
class PingMessage: public MessageTmpl < 10, PingMessage >
{
public:
//PingMessage () {}
virtual void say ()
{
enable (); // virtual (not static) function of the template class
printf ("Ping\n");
}
};
Ответ 5
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 >();
//
Или в вашем текущем коде просто используйте разумный размер распределения и проверяйте границы времени выполнения, чтобы увидеть, что слишком много регистраций. И, возможно, поставьте метод "Отменить регистрацию".
Ответ 6
здесь немного изменено с использованием карты
#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;
}
Ответ 7
Для всех классов сообщений, которые являются подклассами 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();
}