Я пытаюсь создать общий механизм передачи событий в С++, но не ограничиваясь зерном относительно "нового стиля" С++, и в то же время не перейдя за рамки шаблонов.
Мой пример использования несколько специфичен тем, что мне требуется полный контроль над распределением событий. Система событий лежит в основе мирового моделирования, где каждая итерация мира действует на события, созданные предыдущим кадром. Поэтому мне нужно, чтобы все события были помещены в очередь до их отправки, так что приложение может сбрасывать очередь через определенные интервалы времени, что несколько напоминает классический цикл событий GUI.
Мой пример использования тривиален для реализации в Ruby, Python или даже C, но с С++ я немного отстаю. Я посмотрел на Boost:: Signal и другие подобные библиотеки, но они кажутся слишком сложными или негибкими в соответствии с моим конкретным вариантом использования. (Boost, в частности, часто базируется на шаблонах, вплоть до полной путаницы, особенно таких, как boost:: bind или boost:: function.)
Здесь система с широкими штрихами:
-
Потребители прослушивают события, регистрируя себя непосредственно с объектами, которые создают события.
-
События - это просто имена строк, но каждое событие может содержать дополнительные данные.
-
Слушатели - это просто методы. Если бы это был С++ 11, я бы использовал lambdas, но мне нужна широкая переносимость компилятора, поэтому, используя методы на данный момент.
-
Когда производитель запускает событие, событие переходит в очередь, пока не придет время отправить его в список слушателей.
-
Очередь отправляется в строгом порядке запуска событий. (Так что, если производитель A запускает событие x, производитель B запускает y, а продление B триггеры z снова, тогда полный порядок - x, y, z.)
-
Важно, чтобы любые события, создаваемые слушателями во время, не отправлялись до следующей итерации - так что внутри есть действительно две очереди.
Здесь "идеальный" пример псевдокода потребителя:
SpaceshipController::create() {
spaceship.listen("crash", &on_crash);
}
SpaceshipController::on_crash(CrashEvent event) {
spaceship.unlisten("crash", &on_crash);
spaceship.remove();
add(new ExplosionDebris);
add(new ExplosionSound);
}
И вот продюсер:
Spaceship::collided_with(CollisionObject object) {
trigger("crash", new CrashEvent(object));
}
Все это хорошо и хорошо, но перевод на современный С++ - это то, где я встречаю трудности.
Проблема заключается в том, что либо нужно идти с С++ старого стиля с литьем полиморфных экземпляров и уродством, либо нужно идти с полиморфизмом на уровне шаблона с типизированным набором времени.
Я экспериментировал с использованием boost:: bind(), и я могу создать метод прослушивания следующим образом:
class EventManager
{
template <class ProducerClass, class ListenerClass, class EventClass>
void EventManager::listen(
shared_ptr<ProducerClass> producer,
string event_name,
shared_ptr<ListenerClass> listener,
void (ListenerClass::*method)(EventClass* event)
)
{
boost::function1<void, EventClass*> bound_method = boost::bind(method, listener, _1);
// ... add handler to a map for later execution ...
}
}
(Обратите внимание, как я определяю центральный диспетчер событий, потому что мне нужно поддерживать одну очередь для всех производителей. Для удобства отдельные классы все еще наследуют mixin, который предоставляет функции listen() и trigger(), которые делегируют событие менеджер.)
Теперь можно слушать, делая:
void SpaceshipController::create()
{
event_manager.listen(spaceship, "crash", shared_from_this(),
&SpaceshipController::on_crash);
}
void SpaceshipController::on_crash(CrashEvent* event)
{
// ...
}
Это довольно хорошо, хотя это многословно. Я ненавижу заставлять каждый класс наследовать enable_shared_from_this, а С++ требует, чтобы ссылки на методы включали имя класса, которое отстойно, но обе проблемы, вероятно, неизбежны.
К сожалению, я не вижу, как реализовать listen() таким образом, поскольку классы известны только во время компиляции. Мне нужно сохранить слушателей на карте каждого производителя, которая, в свою очередь, содержит карту для каждого события, например:
unordered_map<shared_ptr<ProducerClass>,
unordered_map<string, vector<boost:function1<void, EventClass*> > > > listeners;
Но, конечно, С++ не позволяет мне. Я могу обмануть:
unordered_map<shared_ptr<void*>,
unordered_map<string, vector<boost:function1<void, void*> > > > listeners;
но тогда это выглядит ужасно грязным.
Итак, теперь я должен templatize EventManager или что-то еще, и, возможно, сохранить его для каждого продюсера? Но я не вижу, как это сделать, не разбивая очередь, и я не могу этого сделать.
Обратите внимание на то, что я явно пытаюсь избежать определения чистых классов интерфейса для каждого типа событий, Java-стиль:
class CrashEventListener
{
virtual void on_crash(CrashEvent* event) = 0;
}
С количеством событий, которые я имею в виду, это будет ужасно, быстро.
Также возникает другая проблема: я хочу иметь мелкомасштабный контроль над обработчиками событий. Например, есть много продюсеров, которые просто предоставляют событие под названием "изменение". Я хочу, чтобы иметь возможность подключить продюсер. Событие "change" to on_a_change и, например, производитель B "change" event to on_b_change. Интерфейсы для каждого события сделали бы это неудобно в лучшем случае.
Имея это в виду, кто-то может указать мне в правильном направлении?