在C ++中devise一个事件机制

我正在尝试在C ++中devise一个通用的(但有点用例的)事件传递机制,而不是针对“新风格”C ++的谷物,同时也不用过度使用模板。

我的用例有点特别,因为我需要完全控制事件发布的时间。 事件系统是世界模拟的基础,世界的每一次迭代都会对前一帧产生的事件起作用。 所以我要求所有事件在被分派之前排队等候,这样应用程序就可以按照特定的时间间隔刷新队列,就像传统的GUI事件循环一样。

我的用例在Ruby,Python甚至C中实现是微不足道的,但是用C ++来说,我有点儿短了。 我曾经看过Boost :: Signal和其他类似的库,但是它们看起来太复杂或不灵活,不适合我的特殊用例。 (Boost,尤其是基于模板的语言,经常会导致混淆,特别是boost :: bind或boost :: function)。


这是系统,广泛的笔触:

  • 消费者通过直接注册产生事件的对象来收听事件。

  • 事件只是string名称,但每个事件都可能有附加的数据。

  • 听众只是方法。 如果这是C ++ 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)); } 

所有这一切都很好,但是翻译成现代C ++是我遇到困难的地方。


问题在于,要么使用旧式的C ++,而要使用多态的实例和丑陋,要么使用模板级的多态来进行编译时定义的input。

我已经尝试使用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 ... } } 

(注意我是如何定义中心事件pipe理器的;这是因为我需要在所有生产者中维护一个队列。为了方便起见,单个类仍然inheritance了一个mixin,它提供了listen()和trigger()来委派给事件pipe理器。

现在可以通过下面的方式来听:

 void SpaceshipController::create() { event_manager.listen(spaceship, "crash", shared_from_this(), &SpaceshipController::on_crash); } void SpaceshipController::on_crash(CrashEvent* event) { // ... } 

这很不错,虽然是冗长的。 我讨厌强迫每个类inheritanceenable_shared_from_this,并且C ++要求方法引用包含类名,这很吸引人,但是这两个问题都可能是不可避免的。

不幸的是,我不知道如何以这种方式实现listen(),因为类只在编译时才知道。 我需要将这些侦听器存储在每个生产者映射中,而每个映射又包含一个每个事件名称映射,如下所示:

 unordered_map<shared_ptr<ProducerClass>, unordered_map<string, vector<boost:function1<void, EventClass*> > > > listeners; 

但是,当然C ++不会让我。 我可以作弊:

 unordered_map<shared_ptr<void*>, unordered_map<string, vector<boost:function1<void, void*> > > > listeners; 

但那样感觉非常肮脏。

所以现在我不得不对EventManager进行模板化,并且为每个制作者保留一个,也许? 但是如果不把队列分开,我不知道该怎么做,而且我也做不到。


请注意,我明确地试图避免为每种types的事件(Java风格)定义纯接口类:

 class CrashEventListener { virtual void on_crash(CrashEvent* event) = 0; } 

随着我脑海中的事件的数量,这将变得糟糕,快速。

这也引发了另一个问题:我想对事件处理程序进行细粒度的控制。 例如,有很多生产者只是简单地提供一个叫做“改变”的事件。 例如,我希望能够将生产者A的“更改”事件挂接到on_a_change,并将生产者的B“更改”事件挂接到on_b_change。 每个事件接口最多只会使这个不方便。


考虑到这一切,有人能指出我的方向吗?

更新:这个答案解释了一个选项,但我认为基于boost::any的这个解决scheme的修改版本更清洁。


首先,让我们想象一下,如果您不需要在事件pipe理器中对事件进行排队,解决scheme的外观如何。 也就是说,让我们想象一下,只要有事件要报告,所有的“飞船”都可以实时地向适当的听众发出信号。

在这种情况下,最简单的解决scheme是让每个太空船拥有几个boost :: signals,这些listeners可以连接到这个信号。 当一艘船想报告事件时,它只是发出相应的信号。 (也就是说,通过运算符()调用信号,就像它是一个函数一样)。

这个系统会碰到几个要点(消费者直接向事件生产者注册,而处理程序只是方法),但是它不能解决事件队列问题。 幸运的是,有一个简单的解决scheme。

当事件生产者(即飞船)想要通知他的听众一个事件时,他不应该自己发射信号。 相反,他应该使用boost :: bind打包信号调用,并将生成的函数对象传递给事件处理函数(以boost :: function的forms),并将其附加到队列中。 这样,给事件处理程序的所有事件都只是以下types: boost::function<void ()>

当冲洗队列的时候,事件处理程序只是调用队列中的所有打包事件,每个打包事件本质上都是一个callback函数,为特定事件调用生产者(飞船)的信号。

这是一个完整的示例实现。 main()函数演示了一个简单的“模拟”工作系统。 我甚至在事件pipe理器中抛出了一些互斥锁,因为我认为他可能被多个线程访问。 我并没有为控制器或太空船做同样的事情。 显然,main()函数中提供的简单单线程testing并不能执行事件pipe理器的线程安全性,但是没有什么复杂的事情发生在那里。

最后,你会注意到我包含了两种不同types的事件。 两个示例事件(崩溃和哗变)期望是调用具有自定义签名的方法(基于与该事件相关联的信息的types)。 其他事件(起飞和降落)是“通用的”。 当订阅通用事件时,监听器提供一个string(事件名称)。

总之,这个实现满足你所有的要点。 (通过引入通用事件的例子来满足第二点要点。)如果你想用一个额外的参数“EventInfo”来扩充“通用”信号types,那么可以很容易地完成。

请注意,这里只有一个监听器(控制器),但实现中没有任何内容会限制监听器的数量。 你可以添加尽可能多的,如你所愿。 但是,您必须确保您仔细pipe理生产者(太空船)的生命周期。

还有一件事:既然你对从太空船inheritanceenable_shared_from_thisexpression了一些蔑视,我在订阅的时候将spacehip对象(通过weak_ptr)绑定到信号处理程序中。 这样一来,飞船在发射信号时就不需要明确地向听众提供自己的手柄。

顺便说一下,main()中的BEGIN / END Orbit输出语句就是为了向您显示在事件pipe理器被触发之前事件没有被监听器接收。

(作为参考:这个编译使用gcc和boost 1.46,但应该使用老版本的boost。)

 #include <iostream> #include <vector> #include <string> #include <set> #include <map> #include <boost/bind.hpp> #include <boost/function.hpp> #include <boost/signals2.hpp> #include <boost/foreach.hpp> #include <boost/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/lexical_cast.hpp> // Forward declarations class Spaceship; typedef boost::shared_ptr<Spaceship> SpaceshipPtr; typedef boost::weak_ptr<Spaceship> SpaceshipWPtr; class EventManager; typedef boost::shared_ptr<EventManager> EventManagerPtr; class EventManager { public: // Notify listeners of all recent events void TriggerAllQueuedEvents() { NotificationVec vecNotifications; // Open a protected scope to modify the notification list { // One thread at a time boost::recursive_mutex::scoped_lock lock( m_notificationProtection ); // Copy the notification vector to our local list and clear it at the same time std::swap( vecNotifications, m_vecQueuedNotifications ); } // Now loop over the notification callbacks and call each one. // Since we're looping over the copy we just made, new events won't affect us. BOOST_FOREACH( const EventNotificationFn & fn, vecNotifications ) { fn() ; } } // Callback signature typedef void EventNotificationFnSignature(); typedef boost::function<EventNotificationFnSignature> EventNotificationFn; //! Queue an event with the event manager void QueueEvent( const EventNotificationFn & event ) { // One thread at a time. boost::recursive_mutex::scoped_lock lock( m_notificationProtection ); m_vecQueuedNotifications.push_back(event); } private: // Queue of events typedef std::vector<EventNotificationFn> NotificationVec ; NotificationVec m_vecQueuedNotifications; // This mutex is used to ensure one-at-a-time access to the list of notifications boost::recursive_mutex m_notificationProtection ; }; class Spaceship { public: Spaceship(const std::string & name, const EventManagerPtr & pEventManager) : m_name(name) , m_pEventManager(pEventManager) { } const std::string& name() { return m_name; } // Define what a handler for crash events must look like typedef void CrashEventHandlerFnSignature(const std::string & sound); typedef boost::function<CrashEventHandlerFnSignature> CrashEventHandlerFn; // Call this function to be notified of crash events boost::signals2::connection subscribeToCrashEvents( const CrashEventHandlerFn & fn ) { return m_crashSignal.connect(fn); } // Define what a handler for mutiny events must look like typedef void MutinyEventHandlerFnSignature(bool mutinyWasSuccessful, int numDeadCrew); typedef boost::function<MutinyEventHandlerFnSignature> MutinyEventHandlerFn; // Call this function to be notified of mutiny events boost::signals2::connection subscribeToMutinyEvents( const MutinyEventHandlerFn & fn ) { return m_mutinySignal.connect(fn); } // Define what a handler for generic events must look like typedef void GenericEventHandlerFnSignature(); typedef boost::function<GenericEventHandlerFnSignature> GenericEventHandlerFn; // Call this function to be notified of generic events boost::signals2::connection subscribeToGenericEvents( const std::string & eventType, const GenericEventHandlerFn & fn ) { if ( m_genericEventSignals[eventType] == NULL ) { m_genericEventSignals[eventType].reset( new GenericEventSignal ); } return m_genericEventSignals[eventType]->connect(fn); } void CauseCrash( const std::string & sound ) { // The ship has crashed. Queue the event with the event manager. m_pEventManager->QueueEvent( boost::bind( boost::ref(m_crashSignal), sound ) ); //< Must use boost::ref because signal is noncopyable. } void CauseMutiny( bool successful, int numDied ) { // A mutiny has occurred. Queue the event with the event manager m_pEventManager->QueueEvent( boost::bind( boost::ref(m_mutinySignal), successful, numDied ) ); //< Must use boost::ref because signal is noncopyable. } void CauseGenericEvent( const std::string & eventType ) { // Queue the event with the event manager m_pEventManager->QueueEvent( boost::bind( boost::ref(*m_genericEventSignals[eventType]) ) ); //< Must use boost::ref because signal is noncopyable. } private: std::string m_name; EventManagerPtr m_pEventManager; boost::signals2::signal<CrashEventHandlerFnSignature> m_crashSignal; boost::signals2::signal<MutinyEventHandlerFnSignature> m_mutinySignal; // This map needs to use ptrs, because std::map needs a value type that is copyable // (boost signals are noncopyable) typedef boost::signals2::signal<GenericEventHandlerFnSignature> GenericEventSignal; typedef boost::shared_ptr<GenericEventSignal> GenericEventSignalPtr; std::map<std::string, GenericEventSignalPtr > m_genericEventSignals; }; class Controller { public: Controller( const std::set<SpaceshipPtr> & ships ) { // For every ship, subscribe to all of the events we're interested in. BOOST_FOREACH( const SpaceshipPtr & pSpaceship, ships ) { m_ships.insert( pSpaceship ); // Bind up a weak_ptr in the handler calls (using a shared_ptr would cause a memory leak) SpaceshipWPtr wpSpaceship(pSpaceship); // Register event callback functions with the spaceship so he can notify us. // Bind a pointer to the particular spaceship so we know who originated the event. boost::signals2::connection crashConnection = pSpaceship->subscribeToCrashEvents( boost::bind( &Controller::HandleCrashEvent, this, wpSpaceship, _1 ) ); boost::signals2::connection mutinyConnection = pSpaceship->subscribeToMutinyEvents( boost::bind( &Controller::HandleMutinyEvent, this, wpSpaceship, _1, _2 ) ); // Callbacks for generic events boost::signals2::connection takeoffConnection = pSpaceship->subscribeToGenericEvents( "takeoff", boost::bind( &Controller::HandleGenericEvent, this, wpSpaceship, "takeoff" ) ); boost::signals2::connection landingConnection = pSpaceship->subscribeToGenericEvents( "landing", boost::bind( &Controller::HandleGenericEvent, this, wpSpaceship, "landing" ) ); // Cache these connections to make sure we get notified m_allConnections[pSpaceship].push_back( crashConnection ); m_allConnections[pSpaceship].push_back( mutinyConnection ); m_allConnections[pSpaceship].push_back( takeoffConnection ); m_allConnections[pSpaceship].push_back( landingConnection ); } } ~Controller() { // Disconnect from any signals we still have BOOST_FOREACH( const SpaceshipPtr pShip, m_ships ) { BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pShip] ) { conn.disconnect(); } } } private: typedef std::vector<boost::signals2::connection> ConnectionVec; std::map<SpaceshipPtr, ConnectionVec> m_allConnections; std::set<SpaceshipPtr> m_ships; void HandleGenericEvent( SpaceshipWPtr wpSpaceship, const std::string & eventType ) { // Obtain a shared ptr from the weak ptr SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << "Event on " << pSpaceship->name() << ": " << eventType << '\n'; } void HandleCrashEvent(SpaceshipWPtr wpSpaceship, const std::string & sound) { // Obtain a shared ptr from the weak ptr SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << pSpaceship->name() << " crashed with sound: " << sound << '\n'; // That ship is dead. Delete it from the list of ships we track. m_ships.erase(pSpaceship); // Also, make sure we don't get any more events from it BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pSpaceship] ) { conn.disconnect(); } m_allConnections.erase(pSpaceship); } void HandleMutinyEvent(SpaceshipWPtr wpSpaceship, bool mutinyWasSuccessful, int numDeadCrew) { SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << (mutinyWasSuccessful ? "Successful" : "Unsuccessful" ) ; std::cout << " mutiny on " << pSpaceship->name() << "! (" << numDeadCrew << " dead crew members)\n"; } }; int main() { // Instantiate an event manager EventManagerPtr pEventManager( new EventManager ); // Create some ships to play with int numShips = 5; std::vector<SpaceshipPtr> vecShips; for (int shipIndex = 0; shipIndex < numShips; ++shipIndex) { std::string name = "Ship #" + boost::lexical_cast<std::string>(shipIndex); SpaceshipPtr pSpaceship( new Spaceship(name, pEventManager) ); vecShips.push_back(pSpaceship); } // Create the controller with our ships std::set<SpaceshipPtr> setShips( vecShips.begin(), vecShips.end() ); Controller controller(setShips); // Quick-and-dirty "simulation" // We'll cause various events to happen to the ships in the simulation, // And periodically flush the events by triggering the event manager std::cout << "BEGIN Orbit #1" << std::endl; vecShips[0]->CauseGenericEvent("takeoff"); vecShips[0]->CauseCrash("Kaboom!"); vecShips[1]->CauseGenericEvent("takeoff"); vecShips[1]->CauseCrash("Blam!"); vecShips[2]->CauseGenericEvent("takeoff"); vecShips[2]->CauseMutiny(false, 7); std::cout << "END Orbit #1" << std::endl; pEventManager->TriggerAllQueuedEvents(); std::cout << "BEGIN Orbit #2" << std::endl; vecShips[3]->CauseGenericEvent("takeoff"); vecShips[3]->CauseMutiny(true, 2); vecShips[3]->CauseGenericEvent("takeoff"); vecShips[4]->CauseCrash("Splat!"); std::cout << "END Orbit #2" << std::endl; pEventManager->TriggerAllQueuedEvents(); std::cout << "BEGIN Orbit #3" << std::endl; vecShips[2]->CauseMutiny(false, 15); vecShips[2]->CauseMutiny(true, 20); vecShips[2]->CauseGenericEvent("landing"); vecShips[3]->CauseCrash("Fizzle"); vecShips[3]->CauseMutiny(true, 0); //< Should not cause output, since this ship has already crashed! std::cout << "END Orbit #3" << std::endl; pEventManager->TriggerAllQueuedEvents(); return 0; } 

运行时,上述程序产生以下输出:

 BEGIN Orbit #1 END Orbit #1 Event on Ship #0: takeoff Ship #0 crashed with sound: Kaboom! Event on Ship #1: takeoff Ship #1 crashed with sound: Blam! Event on Ship #2: takeoff Unsuccessful mutiny on Ship #2! (7 dead crew members) BEGIN Orbit #2 END Orbit #2 Event on Ship #3: takeoff Successful mutiny on Ship #3! (2 dead crew members) Event on Ship #3: takeoff Ship #4 crashed with sound: Splat! BEGIN Orbit #3 END Orbit #3 Unsuccessful mutiny on Ship #2! (15 dead crew members) Successful mutiny on Ship #2! (20 dead crew members) Event on Ship #2: landing Ship #3 crashed with sound: Fizzle 

这差不多一年之后,但没有任何答案,所以这里采用了不依赖于RTTI的另一种方法(实际上不需要这样做)。

  • 所有的事件都来自一个基本的事件类,它提供了一个虚函数来检索一个UID
  • 所有来自该类的事件都必须在定义中有一个macros来实现一些“魔术”

     class EventFoo : public IEvent { public: IMPLEMENT_EVENT(EventFoo) // Regular EventFoo specific stuff }; 
  • macros需要注意实现上面提到的虚函数以及实现返回相同UID的静态函数

     typedef unsigned char* EventUID; #define IMPLEMENT_EVENT(Clazz) \ static EventUID StaticGetUID() { \ static unsigned char sUID = 0; \ return (EventUID)&sUID; /* This will be unique in the executable! */ \ } \ virtual EventUID GetUID() const { return StaticGetUID(); } 
  • 请注意,使用这种方法支持单一事件inheritance也是微不足道的(这里的静态无符号字符仅仅作为getto RTTI来避免为了这个而启用编译)

  • 监听器实现一个OnEvent(IEvent&_Event)的函数。

  • 听众在定义中多了一些macros来做间接

     #define EVENT_BINDING_START() virtual void OnEvent(IEvent& _Event) { #define EVENT_BIND(Function, EventType) if (_Event->GetUID() == EventType::StaticGetUID()) Function(static_cast<EventType&>(_Event)); return; /* return right away to handle event */ #define EVENT_BINDING_END(BaseClazz) BaseClazz::OnEvent(_Event); } /* If not handled by us, forward call to parent class */ class Listener : public IEventHandler { public: EVENT_BINDING_START EVENT_BIND(OnFoo, EventFoo) EVENT_BINDING_END(IEventHandler) void OnFoo(EventFoo& _Foo) { /* do stuff */ } }; 

注册事件是相当微不足道的,因为您只需要在某处保留一个IEventHandler *列表。 OnEvent(..)成为一个巨大的开关/ if-else混乱,但你放松了自己实现它。 声明也是相当干净的使用macros。 你也可以自己手动实现OnEvent()。 速度明智,我不会太担心。 对于大多数编译器来说,性能将非常接近switch语句,除非您在单个侦听器中处理大量事件,否则它应该非常快。 您还可以在macros中本地cachingUID值,以避免在侦听器中调用每个事件types的虚拟对象。 在事件的第一个虚拟函数调用之后,vtable将在处理器caching中,任何后续的调用将会非常快。 StaticGetUID函数几乎总是在发布版本中内联,以简单地返回一个常量。 这最终使OnEvent代码相当快速和紧凑。

程序集在x64和powerpc(对于macros存根)也是非常干净的,对x86不太确定。 如果你真的需要的话,这使得进入macros观相当无痛。

这种方法在运行时是types安全的,因为即使具有相同名称的2个事件也具有不同的UID。 请注意,您也可以使用散列algorithm来生成UID或其他方法。

好的,这是我之前缺less的一个相当简单的解决scheme。 这是要走的路。 诺言。

让我重新提出这个问题,并把它分解成可以单独解决的问题。

系统我正在实施一个系统,“听众”将自己注册到“生产者”事件中。 它基本上是一个标准的“ 观察者 ”模式(又名“信号和插槽”),但有一些曲折。

在C ++中,pipe理侦听器和事件生成器之间的连接最简单的方法是什么?

我build议使用现有的库。 boost :: signals或者boost :: signals2会很好地工作。 当然,你可以推出自己的信号和插槽实施,但为什么? boost :: signals为您提供了一个干净的,经过testing的,通用的和有文档的解决scheme,许多其他c ++程序员在查看您的代码时会立即明白。

我的每个生产者都能够产生几种不同types的事件,这意味着我的听众function将会有不同的签名,对吗? 由于boost :: signal的types取决于处理函数的签名 ,因此每个生产者都必须拥有几种不同types的信号。 我将无法把它们放在一个集合中(例如一个地图),这意味着每个集合都必须单独声明。 更糟的是,我必须为每个单独的信号声明一个单独的“getter”函数,以便聆听者可以连接到它。 谈谈样板! 我怎样才能避免呢?

这是棘手的部分。

正如你在你的问题中提到的那样,一个“解决scheme”就是让你的信号作为void *types发出。 而你是对的,那完全是肮脏的。 正如我对这个问题的其他答案所示,有一种types安全的方法可以避免为每个事件定义一个单独的信号。 如果你顺着这条路走,编译器会抓住你所犯的任何错误,但是代码是有点丑陋的。

但是这引出了一个问题:在编译时捕获types错误真的非常重要吗? 使用“肮脏的”无效*技巧的问题是,你永远不会知道你是否犯了错误,直到太晚为止。 如果将处理程序连接到错误types的事件,则行为是不确定的。

Boost提供了一个名为boost::any的库来解决这个问题。 它在概念上类似于void *方法,但是让你知道是否有问题。 如果你使用boost :: any,那么你所有的处理函数都会有相同的函数签名: void (const boost::any &) 。 当然,如果你连接错误的处理程序到一个特定的事件,编译器将不会为你标记它。 但是当你testing的时候你会发现很快。 这是因为boost::any会抛出一个exception,如果你试图把它转换成错误的types。 你的代码将没有繁琐的样板,没有错误会被忽视(假设你的testing是相当完整的)。

注意:boost :: any需要你在RTTI打开的情况下编译你的代码。

好的,但是我的系统有一个怪癖。 我不能让制作人实时通知他们的听众。 我需要以某种方式将事件排队,并定期刷新队列。

这部分的答案大部分与您select将制作者与听众连接的系统无关。 只需使用boost::bind将你的事件通知函数变成一个“ thunk ”,以后可以由你的事件pipe理器执行。 由于所有的thunk都有签名void () ,所以你的事件pipe理器很容易保存当前排队并等待执行的事件通知列表。

以下是使用上述技术的完整示例实现。

 #include <iostream> #include <vector> #include <string> #include <set> #include <map> #include <boost/bind.hpp> #include <boost/function.hpp> #include <boost/signals2.hpp> #include <boost/foreach.hpp> #include <boost/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/lexical_cast.hpp> #include <boost/static_assert.hpp> #include <boost/any.hpp> // Forward declarations class Spaceship; typedef boost::shared_ptr<Spaceship> SpaceshipPtr; typedef boost::weak_ptr<Spaceship> SpaceshipWPtr; class EventManager; typedef boost::shared_ptr<EventManager> EventManagerPtr; // ****************************************************************** // EVENT DEFINITIONS // ****************************************************************** struct TakeoffEvent { static const std::string name ; }; const std::string TakeoffEvent::name = "takeoff" ; struct LandingEvent { static const std::string name ; }; const std::string LandingEvent::name = "landing" ; struct CrashEvent { static const std::string name ; CrashEvent(const std::string & s) : sound(s) {} std::string sound ; }; const std::string CrashEvent::name = "crash" ; struct MutinyEvent { static const std::string name ; MutinyEvent(bool s, int n) : successful(s) , numDead(n) {} bool successful ; int numDead ; }; const std::string MutinyEvent::name = "mutiny" ; // ****************************************************************** // ****************************************************************** class EventManager { public: // Notify listeners of all recent events void FlushAllQueuedEvents() { NotificationVec vecNotifications; // Open a protected scope to modify the notification list { // One thread at a time boost::recursive_mutex::scoped_lock lock( m_notificationProtection ); // Copy the notification vector to our local list and clear it at the same time std::swap( vecNotifications, m_vecQueuedNotifications ); } // Now loop over the notification callbacks and call each one. // Since we're looping over the copy we just made, new events won't affect us. BOOST_FOREACH( const NamedNotification & nameAndFn, vecNotifications ) { // Debug output std::cout << "Flushing " << nameAndFn.first << std::endl ; try { // call the listener(s) nameAndFn.second() ; } catch ( const boost::bad_any_cast & ) { std::cout << "*** BUG DETECTED! Invalid any_cast. ***" << std::endl ; } } } // Callback signature typedef void EventNotificationFnSignature(); typedef boost::function<EventNotificationFnSignature> EventNotificationFn; //! Queue an event with the event manager void QueueEvent( const std::string & name, const EventNotificationFn & nameAndEvent ) { // One thread at a time. boost::recursive_mutex::scoped_lock lock( m_notificationProtection ); m_vecQueuedNotifications.push_back( NamedNotification(name, nameAndEvent) ); } private: // Queue of events typedef std::pair<std::string, EventNotificationFn> NamedNotification ; typedef std::vector<NamedNotification> NotificationVec ; NotificationVec m_vecQueuedNotifications; // This mutex is used to ensure one-at-a-time access to the list of notifications boost::recursive_mutex m_notificationProtection ; }; class EventProducer { public: EventProducer( const EventManagerPtr & pEventManager ) : m_pEventManager(pEventManager) {} typedef void SignalSignature(const boost::any &) ; typedef boost::function<SignalSignature> HandlerFn ; boost::signals2::connection subscribe( const std::string & eventName, const HandlerFn & fn ) { // Create this signal if it doesn't exist yet if ( m_mapSignals.find(eventName) == m_mapSignals.end() ) { m_mapSignals[eventName].reset( new EventSignal ) ; } return m_mapSignals[eventName]->connect(fn) ; } template <typename _Event> void trigger(const _Event & event) { // Do we have a signal for this (if not, then we have no listeners) EventSignalMap::iterator iterFind = m_mapSignals.find(event.name) ; if ( iterFind != m_mapSignals.end() ) { EventSignal & signal = *iterFind->second ; // Wrap the event in a boost::any boost::any wrappedEvent = event ; m_pEventManager->QueueEvent( event.name, boost::bind( boost::ref(signal), wrappedEvent ) ) ; } } protected: typedef boost::signals2::signal<SignalSignature> EventSignal ; typedef boost::shared_ptr<EventSignal> EventSignalPtr ; typedef std::map<std::string, EventSignalPtr> EventSignalMap ; EventSignalMap m_mapSignals ; EventManagerPtr m_pEventManager ; }; typedef boost::shared_ptr<EventProducer> EventProducerPtr ; class Spaceship : public EventProducer { public: Spaceship(const std::string & name, const EventManagerPtr & pEventManager) : EventProducer(pEventManager) , m_name(name) { } std::string & name() { return m_name ; } private: std::string m_name; }; class Listener { public: Listener( const std::set<SpaceshipPtr> & ships ) { // For every ship, subscribe to all of the events we're interested in. BOOST_FOREACH( const SpaceshipPtr & pSpaceship, ships ) { m_ships.insert( pSpaceship ); // Bind up a weak_ptr in the handler calls (using a shared_ptr would cause a memory leak) SpaceshipWPtr wpSpaceship(pSpaceship); // Register event callback functions with the spaceship so he can notify us. // Bind a pointer to the particular spaceship so we know who originated the event. m_allConnections[pSpaceship].push_back( pSpaceship->subscribe( CrashEvent::name, boost::bind( &Listener::HandleCrashEvent, this, wpSpaceship, _1 ) ) ); m_allConnections[pSpaceship].push_back( pSpaceship->subscribe( MutinyEvent::name, boost::bind( &Listener::HandleMutinyEvent, this, wpSpaceship, _1 ) ) ); m_allConnections[pSpaceship].push_back( pSpaceship->subscribe( TakeoffEvent::name, boost::bind( &Listener::HandleTakeoffEvent, this, wpSpaceship, _1 ) ) ); m_allConnections[pSpaceship].push_back( pSpaceship->subscribe( LandingEvent::name, boost::bind( &Listener::HandleLandingEvent, this, wpSpaceship, _1 ) ) ); // Uncomment this next line to see what happens if you try to connect a handler to the wrong event. // (Connecting "landing" event to "crash" handler.) // m_allConnections[pSpaceship].push_back( pSpaceship->subscribe( LandingEvent::name, // boost::bind( &Listener::HandleCrashEvent, this, wpSpaceship, _1 ) ) ); } } ~Listener() { // Disconnect from any signals we still have BOOST_FOREACH( const SpaceshipPtr pShip, m_ships ) { BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pShip] ) { conn.disconnect(); } } } private: typedef std::vector<boost::signals2::connection> ConnectionVec; std::map<SpaceshipPtr, ConnectionVec> m_allConnections; std::set<SpaceshipPtr> m_ships; void HandleTakeoffEvent( SpaceshipWPtr wpSpaceship, const boost::any & wrappedEvent ) { // Obtain a shared ptr from the weak ptr SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << "Takeoff event on " << pSpaceship->name() << '\n'; } void HandleLandingEvent( SpaceshipWPtr wpSpaceship, const boost::any & wrappedEvent ) { // Obtain a shared ptr from the weak ptr SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << "Landing event on " << pSpaceship->name() << '\n'; } void HandleCrashEvent(SpaceshipWPtr wpSpaceship, const boost::any & wrappedEvent ) { // Extract the crash event from the boost::any CrashEvent crash = boost::any_cast<CrashEvent>(wrappedEvent) ; // Obtain a shared ptr from the weak ptr SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << pSpaceship->name() << " crashed with sound: " << crash.sound << '\n'; // That ship is dead. Delete it from the list of ships we track. m_ships.erase(pSpaceship); // Also, make sure we don't get any more events from it BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pSpaceship] ) { conn.disconnect(); } m_allConnections.erase(pSpaceship); } void HandleMutinyEvent(SpaceshipWPtr wpSpaceship, const boost::any & wrappedEvent ) { // Extract the mutiny event from the boost::any MutinyEvent mutiny = boost::any_cast<MutinyEvent>(wrappedEvent) ; SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << (mutiny.successful ? "Successful" : "Unsuccessful" ) ; std::cout << " mutiny on " << pSpaceship->name() << "! (" << mutiny.numDead << " dead crew members)\n"; } }; int main() { // Instantiate an event manager EventManagerPtr pEventManager( new EventManager ); // Create some ships to play with int numShips = 5; std::vector<SpaceshipPtr> vecShips; for (int shipIndex = 0; shipIndex < numShips; ++shipIndex) { std::string name = "Ship #" + boost::lexical_cast<std::string>(shipIndex); SpaceshipPtr pSpaceship( new Spaceship(name, pEventManager) ); vecShips.push_back(pSpaceship); } // Create the controller with our ships std::set<SpaceshipPtr> setShips( vecShips.begin(), vecShips.end() ); Listener controller(setShips); // Quick-and-dirty "simulation" // We'll cause various events to happen to the ships in the simulation, // And periodically flush the events by triggering the event manager std::cout << "BEGIN Orbit #1" << std::endl; vecShips[0]->trigger( TakeoffEvent() ); vecShips[0]->trigger( CrashEvent("Kaboom!") ); vecShips[1]->trigger( TakeoffEvent() ); vecShips[1]->trigger( CrashEvent("Blam!") ); vecShips[2]->trigger( TakeoffEvent() ); vecShips[2]->trigger( MutinyEvent(false, 7) ); std::cout << "END Orbit #1\n" << std::endl; pEventManager->FlushAllQueuedEvents(); std::cout << "\n" ; std::cout << "BEGIN Orbit #2" << std::endl; vecShips[3]->trigger( TakeoffEvent() ); vecShips[3]->trigger( MutinyEvent(true, 2) ); vecShips[4]->trigger( TakeoffEvent() ); vecShips[4]->trigger( CrashEvent("Splat!") ); std::cout << "END Orbit #2\n" << std::endl; pEventManager->FlushAllQueuedEvents(); std::cout << "\n" ; std::cout << "BEGIN Orbit #3" << std::endl; vecShips[2]->trigger( MutinyEvent(false, 15) ); vecShips[2]->trigger( MutinyEvent(true, 20) ); vecShips[2]->trigger( LandingEvent() ); vecShips[3]->trigger( CrashEvent("Fizzle.") ); vecShips[3]->trigger( MutinyEvent(true, 0) ); //< Should not cause output, since this ship has already crashed! std::cout << "END Orbit #3\n" << std::endl; pEventManager->FlushAllQueuedEvents(); std::cout << "\n" ; return 0; } 

You could use a dispatch function, implemented by all listeners. The EventManager would call the dispatch function for all events, and the listener could then decide how to dispatch that event internally.

 void Listener::on_event( Event* event ) { switch (event.type) { case (kCrashEvent): this->on_crash((CrashEvent*)event); ... } } 

Then your listen function would look like:

 void EventManager::listen( Listener* listener, EventType eventType ) { // Store the listener, and the type of event it's listening for ... } 

With this design, EventManager has all the information (including types) that it needs to queue and dispatch events, and you don't have the interface-method explosion you were worried about with the java model. Each listener class just implements their on_event dispatch method appropriately for the kinds of events they want to listen to and how they want to listen to them.

Here's a modified sample implementation that (1) requires less "boilerplate" in the Listener implementation classes, and (2) adds slightly improved debug info when queueing events in the event manager.

The tricky thing is getting a producer to own multiple signals of different types, but using a single function to access them. In this implementation, I use multiple inheritance to achieve this. Yes, yes, I know: it's evil and whatnot. It also happens to work in this instance.

 #include <iostream> #include <vector> #include <string> #include <set> #include <map> #include <boost/bind.hpp> #include <boost/function.hpp> #include <boost/signals2.hpp> #include <boost/foreach.hpp> #include <boost/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/lexical_cast.hpp> #include <boost/static_assert.hpp> // Forward declarations class Spaceship; typedef boost::shared_ptr<Spaceship> SpaceshipPtr; typedef boost::weak_ptr<Spaceship> SpaceshipWPtr; class EventManager; typedef boost::shared_ptr<EventManager> EventManagerPtr; // ****************************************************************** // EVENT DEFINITIONS // ****************************************************************** struct TakeoffEvent { static const std::string name ; }; const std::string TakeoffEvent::name = "takeoff" ; struct LandingEvent { static const std::string name ; }; const std::string LandingEvent::name = "landing" ; struct CrashEvent { static const std::string name ; CrashEvent(const std::string & s) : sound(s) {} std::string sound ; }; const std::string CrashEvent::name = "crash" ; struct MutinyEvent { static const std::string name ; MutinyEvent(bool s, int n) : successful(s) , numDead(n) {} bool successful ; int numDead ; }; const std::string MutinyEvent::name = "mutiny" ; // ****************************************************************** // ****************************************************************** class EventManager { public: // Notify listeners of all recent events void FlushAllQueuedEvents() { NotificationVec vecNotifications; // Open a protected scope to modify the notification list { // One thread at a time boost::recursive_mutex::scoped_lock lock( m_notificationProtection ); // Copy the notification vector to our local list and clear it at the same time std::swap( vecNotifications, m_vecQueuedNotifications ); } // Now loop over the notification callbacks and call each one. // Since we're looping over the copy we just made, new events won't affect us. BOOST_FOREACH( const NamedNotification & nameAndFn, vecNotifications ) { // Debug output std::cout << "Flushing " << nameAndFn.first << std::endl ; // call the listener(s) nameAndFn.second() ; } } // Callback signature typedef void EventNotificationFnSignature(); typedef boost::function<EventNotificationFnSignature> EventNotificationFn; //! Queue an event with the event manager void QueueEvent( const std::string & name, const EventNotificationFn & nameAndEvent ) { // One thread at a time. boost::recursive_mutex::scoped_lock lock( m_notificationProtection ); m_vecQueuedNotifications.push_back( NamedNotification(name, nameAndEvent) ); } private: // Queue of events typedef std::pair<std::string, EventNotificationFn> NamedNotification ; typedef std::vector<NamedNotification> NotificationVec ; NotificationVec m_vecQueuedNotifications; // This mutex is used to ensure one-at-a-time access to the list of notifications boost::recursive_mutex m_notificationProtection ; }; template <typename _Event> class Producer { public: Producer( const EventManagerPtr & pEventManager ) : m_pEventManager(pEventManager) {} typedef void SignalSignature(const _Event &) ; boost::signals2::connection subscribe( const boost::function<SignalSignature> & fn ) { return m_signal.connect(fn) ; } void trigger(const _Event & event) { m_pEventManager->QueueEvent( event.name, boost::bind( boost::ref(m_signal), event ) ) ; } protected: // Instantiate the tuple of signals boost::signals2::signal<SignalSignature> m_signal ; EventManagerPtr m_pEventManager ; }; class Spaceship : public Producer<TakeoffEvent> , public Producer<LandingEvent> , public Producer<CrashEvent> , public Producer<MutinyEvent> { public: Spaceship(const std::string & name, const EventManagerPtr & pEventManager) : Producer<TakeoffEvent>(pEventManager) , Producer<LandingEvent>(pEventManager) , Producer<CrashEvent>(pEventManager) , Producer<MutinyEvent>(pEventManager) , m_name(name) { } std::string & name() { return m_name ; } template <typename _Event> boost::signals2::connection subscribe( const boost::function<void (const _Event &)> & fn ) { // call the correct base class return Producer<_Event>::subscribe( fn ) ; } template <typename _Event> void trigger(const _Event & event = _Event() ) { // call the correct base class Producer<_Event>::trigger(event) ; } private: std::string m_name; }; class Listener { public: Listener( const std::set<SpaceshipPtr> & ships ) { // For every ship, subscribe to all of the events we're interested in. BOOST_FOREACH( const SpaceshipPtr & pSpaceship, ships ) { m_ships.insert( pSpaceship ); // Bind up a weak_ptr in the handler calls (using a shared_ptr would cause a memory leak) SpaceshipWPtr wpSpaceship(pSpaceship); // Register event callback functions with the spaceship so he can notify us. // Bind a pointer to the particular spaceship so we know who originated the event. m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<CrashEvent>( boost::bind( &Listener::HandleCrashEvent, this, wpSpaceship, _1 ) ) ); m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<MutinyEvent>( boost::bind( &Listener::HandleMutinyEvent, this, wpSpaceship, _1 ) ) ); m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<TakeoffEvent>( boost::bind( &Listener::HandleGenericEvent<TakeoffEvent>, this, wpSpaceship, _1 ) ) ); m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<LandingEvent>( boost::bind( &Listener::HandleGenericEvent<LandingEvent>, this, wpSpaceship, _1 ) ) ); } } ~Listener() { // Disconnect from any signals we still have BOOST_FOREACH( const SpaceshipPtr pShip, m_ships ) { BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pShip] ) { conn.disconnect(); } } } private: typedef std::vector<boost::signals2::connection> ConnectionVec; std::map<SpaceshipPtr, ConnectionVec> m_allConnections; std::set<SpaceshipPtr> m_ships; template <typename _Event> void HandleGenericEvent( SpaceshipWPtr wpSpaceship, const _Event & event) { // Obtain a shared ptr from the weak ptr SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << "Event on " << pSpaceship->name() << ": " << _Event::name << '\n'; } void HandleCrashEvent(SpaceshipWPtr wpSpaceship, const CrashEvent & crash) { // Obtain a shared ptr from the weak ptr SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << pSpaceship->name() << " crashed with sound: " << crash.sound << '\n'; // That ship is dead. Delete it from the list of ships we track. m_ships.erase(pSpaceship); // Also, make sure we don't get any more events from it BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pSpaceship] ) { conn.disconnect(); } m_allConnections.erase(pSpaceship); } void HandleMutinyEvent(SpaceshipWPtr wpSpaceship, const MutinyEvent & mutiny ) { SpaceshipPtr pSpaceship = wpSpaceship.lock(); std::cout << (mutiny.successful ? "Successful" : "Unsuccessful" ) ; std::cout << " mutiny on " << pSpaceship->name() << "! (" << mutiny.numDead << " dead crew members)\n"; } }; int main() { // Instantiate an event manager EventManagerPtr pEventManager( new EventManager ); // Create some ships to play with int numShips = 5; std::vector<SpaceshipPtr> vecShips; for (int shipIndex = 0; shipIndex < numShips; ++shipIndex) { std::string name = "Ship #" + boost::lexical_cast<std::string>(shipIndex); SpaceshipPtr pSpaceship( new Spaceship(name, pEventManager) ); vecShips.push_back(pSpaceship); } // Create the controller with our ships std::set<SpaceshipPtr> setShips( vecShips.begin(), vecShips.end() ); Listener controller(setShips); // Quick-and-dirty "simulation" // We'll cause various events to happen to the ships in the simulation, // And periodically flush the events by triggering the event manager std::cout << "BEGIN Orbit #1" << std::endl; vecShips[0]->trigger( TakeoffEvent() ); vecShips[0]->trigger( CrashEvent("Kaboom!") ); vecShips[1]->trigger( TakeoffEvent() ); vecShips[1]->trigger( CrashEvent("Blam!") ); vecShips[2]->trigger( TakeoffEvent() ); vecShips[2]->trigger( MutinyEvent(false, 7) ); std::cout << "END Orbit #1\n" << std::endl; pEventManager->FlushAllQueuedEvents(); std::cout << "\n" ; std::cout << "BEGIN Orbit #2" << std::endl; vecShips[3]->trigger( TakeoffEvent() ); vecShips[3]->trigger( MutinyEvent(true, 2) ); vecShips[4]->trigger( TakeoffEvent() ); vecShips[4]->trigger( CrashEvent("Splat!") ); std::cout << "END Orbit #2\n" << std::endl; pEventManager->FlushAllQueuedEvents(); std::cout << "\n" ; std::cout << "BEGIN Orbit #3" << std::endl; vecShips[2]->trigger( MutinyEvent(false, 15) ); vecShips[2]->trigger( MutinyEvent(true, 20) ); vecShips[2]->trigger( LandingEvent() ); vecShips[3]->trigger( CrashEvent("Fizzle.") ); vecShips[3]->trigger( MutinyEvent(true, 0) ); //< Should not cause output, since this ship has already crashed! std::cout << "END Orbit #3\n" << std::endl; pEventManager->FlushAllQueuedEvents(); std::cout << "\n" ; return 0; } 

The simplest way I always found is similar cases is root all events from a polymorphic empty base (a class with just a virtual structure), with each even as a class carrying the event paramenters:

 struct event { virtual ~event() {} }; struct crash: public event { object* collider; }; 

The dispatcher is a functor that takes a event& and walks a collection (typically an std::list) of polymorphic internal bridges like

 struct bridge { virtual ~bridge() {} virtual bool same_as(const bridge* p) const=0; //to implement unlisten virtual bool on_ev(event& ev)=0; }; template<class E, class T> struct fnbridge: public bridge { T* pt; bool(T::*mfn)(E&); virtual bool on_ev(event& ev) { E* pe = dynamic_cast<E*>(&ev); return pe && (pt->*mfn)(*pe); } virtual bool same_as(const bridge* p) { const fnbridge* pb = dynamic_cast<const fnbridge*>(p); return pb && pb->pt == pt && pb->mfn == mfn; } }; 

Now you can wrap a std::list<bridge*> in a class adding bridges on "listen" (in fact template<class T, class E>void listen(T& t, bool(T::*mfn)(E&) ) and removing on unlisten via remove_if with a predicate that calls same_as . That wrapper is also a functor taking a event, iterating on the list calling on_ev , eventually breaking the loop if returing true.

Every time i tried to avoid the dynamic_cast -si found in fact myself in trying to re-implement it through type-tags etc. so … for runtime solution. let RTTI to play its role.

Qt's event model is instructive

  • there is a central thread which dispatches events
  • you can post events to it synchronously or asynchronously
  • inheritable event handlers correspond to the inheritance tree of the base Event
  • all event handlers are virtual
  • each event handler defines whether it handles the event or not
  • an object can inject its own event handler into a slave object to filter and/or transform the slave's events