Тема избитая, однако попробуем поднять планку ещё выше, т.е. ещё меньше "синтаксического шума" для пользователя.
Задача, что бы для пользователя это выглядело следующим образом:
// описываем класс сообщенияstruct my_msg : msg<my_msg>
{
int data;
my_msg(int data) : data(data) {}
};
// описываем класс получателяclass my_agent : publicagent<my_agent>
{
public:
my_agent(int data) : data(data) {}
void on_event(event<my_msg> ev)
{
std::cout << "on_event: my_agent::data=" << data << ", my_msg::data=" << ev.msg->data << std::endl;
}
private:
int data;
};
// собственно всё, дальше можем отправлять полиморфные сообщения полиморфному получателю
// т.е. ни тип сообщения, ни тип получателя при отправке уже не нужныint main()
{
my_agent d (57);
my_msg m (113);
agent_base* b = &d;
msg_base* mb = &m;
b->send(*mb);
}
Всё, что требуется для регистрации обработчика, — это отнаследовать получателя от CRTP базы и объявить метод обработчика, который принимает специальный объект event<msg_t> (этот event помимо самого сообщения может так же содержать отправителя сообщения и другую контекстную информацию).
Ниже приводится реализация. Большая часть особого интереса не представляет, т.к. занимается раздачей уникальных идентификаторов типам сообщений и типам получателей. Наибольший интерес представляет класс agent<>::event<>, который как раз и регистрирует обработчик с помощью "конструктора класса" (http://www.rsdn.ru/forum/message/3089286.1.aspx
). Суть приёма — метод класса регистрируется за счёт "упоминания" класса event<msg>; при этом класс получателя определяется автоматически за счёт того, что event<> не глобальный класс, а определен в CRTP базе (в противном случае пришлось бы писать void on_event(event<my_agent, my_msg> ev)).
std::map<int, std::map<int, void(*)(void*, void const*)> > handlers;
struct msg_base
{
virtual int get_id() const = 0;
protected:
static int id_seq_;
};
struct agent_base
{
virtual int get_id() const = 0;
void send(msg_base const& m)
{
handlers[get_id()][m.get_id()](this, &m);
}
protected:
static int id_seq_;
};
template<typename derived_t>
class agent : public agent_base
{
protected:
template<typename msg_t>
struct event : class_initializer<event<msg_t> >
{
msg_t const* msg;
agent_base* sender;
private:
friend class class_initializer<event<msg_t> >;
static void static_ctor()
{
handlers[derived_t::static_id()][msg_t::static_id()] = &agent::thunk<msg_t>;
}
};
template<typename msg_t>
static void thunk(void* d, void const* m)
{
event<msg_t> ev;
ev.msg = static_cast<msg_t const*>(static_cast<msg_base const*>(m));
ev.sender = 0; // просто для примераstatic_cast<derived_t*>(static_cast<agent_base*>(d))->on_event(ev);
}
static int id_;
virtual int get_id() const
{
return static_id();
}
public:
static int static_id()
{
if (id_ == 0)
id_ = ++agent_base::id_seq_;
return id_;
}
};
template<typename derived_t>
class msg : public msg_base
{
private:
static int id_;
virtual int get_id() const
{
return static_id();
}
public:
static int static_id()
{
if (id_ == 0)
id_ = ++msg_base::id_seq_;
return id_;
}
};
// определения статических членов (в порядке появления):int msg_base::id_seq_;
int agent_base::id_seq_;
template<typename derived_t> int agent<derived_t>::id_;
template<typename derived_t> int msg<derived_t>::id_;
Единственная засада, которая пока видится, — если класс получателя шаблонный, то он не сможет называть event<T> просто как event<T>, т.к. он будет сидеть в шаблонной базе (не актуально для MSVC — она это позволяет). Решить можно добавлением "using agent<my_agent<T> >::event" в объявление шаблонного получателя.
Здравствуйте, sokel, Вы писали:
S>Супер, только я бы ещё от виртуальности избавился, скопировав id в конструкторе базы.
Я думаю, с этим никаких проблем не будет.
Если в классах и так есть виртуальные функции, то наверное логичнее использовать виртуальные функции для идентификаторов. Если виртуальных функций нет иначе, то можно и хранить их в базовом классе.
... а можно и захачить таблицу виртуальных функций и вместо указателя на функцию get_id() класть туда непосредственно сам идентификатор
E>Правильно ли я понимаю, что для какого-то конкретного типа сообщения M в классе может быть только один его обработчик с именем on_event?
Непосредственно в этой реализации — да.
Но если допустим в какой-то гипотетической библиотеке надо так же иметь возможность иметь несколько обработчиков для одного сообщения в разных состояниях, то можно модифицировать эту схему.
Вариант первый будет выглядеть как:
В принципе тут можно наследовать класс агента от некого специального state_based_agent<derived_t>, что бы отличать таких агентов от простых агентов без состояний. А можно и не наследовать, т.к. я думаю возможно это отличать автоматически.
Здравствуйте, remark, Вы писали:
R>Я думаю, с этим никаких проблем не будет. R>Если в классах и так есть виртуальные функции, то наверное логичнее использовать виртуальные функции для идентификаторов. Если виртуальных функций нет иначе, то можно и хранить их в базовом классе.
Просто так получается что базовый механизм сразу навязывает таблицу виртуальных функций. Понятно, что если они и так будут, мы получим более компактную реализацию. Но с копией идентификатора непосредственно диспетчеризация будет побыстрей
R>... а можно и захачить таблицу виртуальных функций и вместо указателя на функцию get_id() класть туда непосредственно сам идентификатор
Хех, такой изврат мне в голову ни разу не приходил... Это уже конечно не виртуальный вызов, но косвенная адресация всё равно останется.
S>Здравствуйте, remark, Вы писали:
R>>Я думаю, с этим никаких проблем не будет. R>>Если в классах и так есть виртуальные функции, то наверное логичнее использовать виртуальные функции для идентификаторов. Если виртуальных функций нет иначе, то можно и хранить их в базовом классе.
С виртуальностью, кстати, можно не общую таблицу сделать, а отдельные статические для каждого агента. И заменить, соответственно, agent_base::get_id() на agent_base::get_handlers(). А уж если позволить себе handlers[MAX_AGENT_ID][MAX_MSG_ID], то вообще летать будет.
Здравствуйте, sokel, Вы писали:
R>>Я думаю, с этим никаких проблем не будет. R>>Если в классах и так есть виртуальные функции, то наверное логичнее использовать виртуальные функции для идентификаторов. Если виртуальных функций нет иначе, то можно и хранить их в базовом классе.
S>Просто так получается что базовый механизм сразу навязывает таблицу виртуальных функций. Понятно, что если они и так будут, мы получим более компактную реализацию. Но с копией идентификатора непосредственно диспетчеризация будет побыстрей
Базовый механизм этого не навязывает. Это полностью на усмотрение пользователя.
R>>... а можно и захачить таблицу виртуальных функций и вместо указателя на функцию get_id() класть туда непосредственно сам идентификатор S>Хех, такой изврат мне в голову ни разу не приходил... Это уже конечно не виртуальный вызов, но косвенная адресация всё равно останется.
Зато дополнительная память в объекте не расходуется. Лучшего варианта тут просто нет.
Здравствуйте, sokel, Вы писали:
S>>Здравствуйте, remark, Вы писали:
R>>>Я думаю, с этим никаких проблем не будет. R>>>Если в классах и так есть виртуальные функции, то наверное логичнее использовать виртуальные функции для идентификаторов. Если виртуальных функций нет иначе, то можно и хранить их в базовом классе.
S>С виртуальностью, кстати, можно не общую таблицу сделать, а отдельные статические для каждого агента. И заменить, соответственно, agent_base::get_id() на agent_base::get_handlers(). А уж если позволить себе handlers[MAX_AGENT_ID][MAX_MSG_ID], то вообще летать будет.
Сложный вопрос... честно говоря, я не знаю ответа. Увеличенное потрбеление памяти (потенциально мегабайты) тоже рано или поздно скажется на производительности, занимать значительную часть кэша процессора этой таблицей тоже не хочется...
Я думал о чём-то типа такого: пересортировать идентификаторы получателей и сообщений, т.ч. у каждого получателя идентификаторы сообщений будут как можно ближе друг к другу (гипотеза, что это можно сделать достаточно хорошо). Далее для каждого получателя можно хранить не всю строчку из MAX_MSG_ID элементов, а лишь небольшое подмножество MINi..MAXi. Проще показать в коде:
struct receiver_desc_t
{
int min_id;
int max_id;
handler_t* array;
handler_t* get_handler(int msg_id)
{
if (msg_id >= min_id && msg_id < max_id)
return array[msg_id - min_id];
else
return 0;
}
};
Здравствуйте, remark, Вы писали:
R>Сложный вопрос... честно говоря, я не знаю ответа. Увеличенное потрбеление памяти (потенциально мегабайты) тоже рано или поздно скажется на производительности, занимать значительную часть кэша процессора этой таблицей тоже не хочется... R>Я думал о чём-то типа такого: пересортировать идентификаторы получателей и сообщений, т.ч. у каждого получателя идентификаторы сообщений будут как можно ближе друг к другу (гипотеза, что это можно сделать достаточно хорошо). Далее для каждого получателя можно хранить не всю строчку из MAX_MSG_ID элементов, а лишь небольшое подмножество MINi..MAXi.
Я делал что-то подобное в виде обёртки мапа с целочисленными ключами. По заданному ограничению при возможности складывал ссылки на итераторы в массив. А переопределенный find либо брал итератор из массива либо перенаправлял в find map'а. Можно усложнить — сделать реализацию, которая будет даже при превышении ограничения выполнять частичный перевод дерева в табличный вид, типа искать максимальный по количеству диапазон ключей, не превышающий ограничение по разнице min-max, а затем разбивать map на три части — map до min_id, массив, map после max_id.
Здравствуйте, sokel, Вы писали:
S>Я делал что-то подобное в виде обёртки мапа с целочисленными ключами. По заданному ограничению при возможности складывал ссылки на итераторы в массив. А переопределенный find либо брал итератор из массива либо перенаправлял в find map'а. Можно усложнить — сделать реализацию, которая будет даже при превышении ограничения выполнять частичный перевод дерева в табличный вид, типа искать максимальный по количеству диапазон ключей, не превышающий ограничение по разнице min-max, а затем разбивать map на три части — map до min_id, массив, map после max_id.
Неплохой вариант. Напоминает реализацию таблиц в Lua, они там тоже некоторый плотный поддиапазон опционально хранят в массиве, а оставшуюся часть (если есть) в дереве.
Если не жалко времени на реализацию, то такой вариант наверное один из самых лучших, что я видел.
Я думаю, что оставшуюся часть уже можно хранить и в одном дереве (не обязательно бить на 2 части), практически ничего не изменится.
Здравствуйте, SenkraD, Вы писали:
R>> friend class class_initializer<event<msg_t> >; /* У меня это не прошло — это глюс компилера или я таки правильно понимаю как работают "друзья" */ R>> handlers[derived_t::static_id()][msg_t::static_id()] = &agent::thunk<msg_t>;/* и на это компилер тоже матерится — не может понять как взять адрес такой функции. */ SD>Машина: Mac PPC, tiger SD>Компилер: gcc 4.0.2
Я набросал только под MSVC9, если надо под другие компиляторы, то надо допиливать напильником.
Здравствуйте, remark, Вы писали:
R>// собственно всё, дальше можем отправлять полиморфные сообщения полиморфному получателю R>// т.е. ни тип сообщения, ни тип получателя при отправке уже не нужны
Можно я тему подниму? Бывает ведь и такой случай: тип сообщения и тип получателя на момент отправки известны, например, какая нибудь очередь асинхронной обработки сообщений. В этом случае можно всё сделать ещё проще: регистрировать обработчик в шаблонном методе отправки сообщений. А идентификатор сообщения инициализировать непосредственно в момент отправки:
// базовый тип сообщенияclass msg
{
template<typename> friend class msgqueue;
unsigned int id;
};
// сочередь сообщенийtemplate<typename impl_type>
class msgqueue
{
public:
template<typename msg_type>
void post(msg_type* m)
{
msg* b = static_cast<msg*>(m);
b->id = handler<msg_type>::id;
// здесь помещаем в очередь, потом эту очередь откуда то разгребаем
// но это пример, так что сразу синхронно вызовем dispatch
dispatch_message(b);
}
private:
void post(msg*); // закрываем возможность отправки сообщений через базовый тип
// таблица обработчиков сообщений, генерируется при инициализации статикиstatic std::vector<void(*)(void*, void*)> handlers;
// собственно handlertemplate<typename msg_type>
struct handler : static_initializer<handler<msg_type> >
{
static unsigned int id;
// регистрируем обработчик в таблицеstatic void static_ctor()
{
id = (unsigned int) handlers.size();
handlers.push_back(handle);
}
// функция обработки сообщенияstatic void handle(void* q, void* m)
{
static_cast<impl_type*>(q)->on_msg(static_cast<msg_type*>(m));
delete static_cast<msg_type*>(m); // и виртуальный деструктор не требуется
}
};
// метод сопоставления сообщению обработчикаvoid dispatch_message(msg* m)
{
handlers[m->id](this, m);
}
};
// инициализируем статикуtemplate<typename impl_type> std::vector<void(*)(void*, void*)> msgqueue<impl_type>::handlers;
template<typename impl_type> template<typename msg_type> unsigned int msgqueue<impl_type>::handler<msg_type>::id = 0;
ну и пример использования:
struct my_msg : msg
{
my_msg(int i) : i(i) {}
int i;
};
struct my_queue : msgqueue<my_queue>
{
void on_msg(my_msg* m)
{
printf("%d\n", m->i);
}
};
int main()
{
my_queue msgq;
// такая запись заставляет инстанцировать метод my_queue::on_message(my_msg*)
msgq.post(new my_msg(42));
// особенность подхода: нельзя делать post через базу
// msgq.post(static_cast<msg*>(new my_msg(42))); // ошибка - private метод
}
Здравствуйте, sokel, Вы писали:
S>Можно я тему подниму? Бывает ведь и такой случай: тип сообщения и тип получателя на момент отправки известны, например, какая нибудь очередь асинхронной обработки сообщений. В этом случае можно всё сделать ещё проще: регистрировать обработчик в шаблонном методе отправки сообщений. А идентификатор сообщения инициализировать непосредственно в момент отправки:
хотя, в этом случае, можно вообще обойтись и без таблицы и без статики:
// базовый тип сообщенияclass msg
{
template<typename> friend class msgqueue;
void (* handle)(void*, void*);
unsigned int id;
};
// сочередь сообщенийtemplate<typename impl_type>
class msgqueue
{
public:
template<typename msg_type>
void post(msg_type* m)
{
m->handle = handle<msg_type>;
// здесь помещаем в очередь, потом эту очередь откуда то разгребаем
// но это пример, так что сразу вызовем dispatch
dispatch_message(m);
}
private:
void post(msg*); // закрываем возможность отправки сообщений через базовый тип
// функция обработки сообщенияtemplate<typename msg_type>
static void handle(void* q, void* m)
{
static_cast<impl_type*>(q)->on_msg(static_cast<msg_type*>(m));
delete static_cast<msg_type*>(m); // и виртуальный деструктор не требуется
}
// метод обработки сообщенияvoid dispatch_message(msg* m) { m->handle(this, m); }
};
Здравствуйте, sokel, Вы писали:
S>Здравствуйте, sokel, Вы писали:
S>>Можно я тему подниму? Бывает ведь и такой случай: тип сообщения и тип получателя на момент отправки известны, например, какая нибудь очередь асинхронной обработки сообщений. В этом случае можно всё сделать ещё проще: регистрировать обработчик в шаблонном методе отправки сообщений. А идентификатор сообщения инициализировать непосредственно в момент отправки:
S>хотя, в этом случае, можно вообще обойтись и без таблицы и без статики:
В таком случае диспетчеризацию действительно можно сделать достаточно просто. Однако такой подход требует, что бы в месте вызова было видно и определение типа сообщения и определение типа получателя. В каких-то контекстах, это может не быть проблемой. Однако я целюсь на большие приложения, где очень желательно максимально разорвать отправителей и получателей.
Идея следующая: в отдельных заголовочных файлах описываются структуры сообщений. Эти объявления — фактически есть интерфейсы модулей, и они включаются повсеместно, где нужны. Всё остальное — какие классы и как их обрабатывают — никому не видно.