用 C++ 设计事件机制
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/7464025/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Designing an event mechanism in C++
提问by Alexander Staubo
I am trying to design a generic (but somewhat use-case-specific) event-passing mechanism in C++ without going against the grain with regard to "new style" C++, and at the same time without going overboard with templates.
我试图在 C++ 中设计一个通用的(但有点特定于用例的)事件传递机制,而不违背“新风格”C++ 的原则,同时又不过度使用模板。
My use case is somewhat particular in that I require complete control over when events are distributed. The event system underlies a world simulation where each iteration of the world acts on the events generated by the previous frame. So I require all events to be queued up before they are dispatched, so that the app can flush the queue at specific intervals, somewhat like the classic GUI event loop.
我的用例有些特殊,因为我需要完全控制事件的分发时间。事件系统是世界模拟的基础,其中世界的每次迭代都作用于前一帧生成的事件。所以我要求所有事件在分派之前先排队,以便应用程序可以在特定的时间间隔刷新队列,有点像经典的 GUI 事件循环。
My use case is trivial to implement in Ruby, Python or even C, but with C++ I am coming up a bit short. I havelooked at Boost::Signal and other similar libraries, but they seem too complex or inflexible to suit my particular use case. (Boost, in particular, is templated-based often to the point of utter confusion, especially things like boost::bind or boost::function.)
我的用例在 Ruby、Python 甚至 C 中实现起来很简单,但是使用 C++ 我来的有点短。我已经看了的boost ::信号和其它类似的库,但他们似乎过于复杂或不灵活,以适应我特别的使用情况。(尤其是 Boost,它是基于模板的,常常到了完全混乱的地步,尤其是像 boost::bind 或 boost::function 这样的东西。)
Here's the system, in broad strokes:
这是系统,大致如下:
Consumers listen to events by registering themselves directly with the objects that produce the events.
Events are just string names, but each event may have additional data attached to it.
Listeners are just methods. If this were C++11 I would use lambdas, but I need broad compiler portability, so using methods for the moment.
When a producer triggers an event, the event goes into the queue until the time comes to dispatch it to the list of listeners.
The queue is dispatched in strict order of event triggering. (So if producer A triggers event x, an producer B triggers y, and producer B triggers z again, then the total order is x, y, z.)
It's important that whatever events are produced by the listeners during will not be dispatched until the nextiteration -- so there are really two queues internally.
消费者通过直接向产生事件的对象注册自己来监听事件。
事件只是字符串名称,但每个事件可能附加有附加数据。
监听器只是方法。如果这是 C++11,我会使用 lambdas,但我需要广泛的编译器可移植性,所以暂时使用方法。
当生产者触发事件时,该事件进入队列,直到将其分派到侦听器列表的时间到来。
队列按事件触发的严格顺序分派。(所以如果生产者A触发事件x,生产者B触发y,生产者B再次触发z,那么总顺序为x,y,z。)
重要的是,在下一次迭代之前,侦听器产生的任何事件都不会被分派——因此内部确实有两个队列。
Here's an "ideal" pseudo-code example of a consumer:
这是消费者的“理想”伪代码示例:
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);
}
And here is a producer:
这是一个生产者:
Spaceship::collided_with(CollisionObject object) {
trigger("crash", new CrashEvent(object));
}
All this is well and good, but translating into modern C++ is where I meet difficulty.
所有这些都很好,但转换成现代 C++ 是我遇到困难的地方。
The problem is that either one has to go with old-style C++ with casting polymorphic instances and ugliness, or one has to go with template-level polymorphism with compile-time defined typing.
问题在于,要么必须使用带有强制转换多态实例和丑陋的旧式 C++,要么必须使用具有编译时定义类型的模板级多态性。
I have experimented with using boost::bind(), and I can produce a listen method like this:
我已经尝试过使用 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 ...
}
}
(Note how I am defining central event manager; that is because I need to maintain a single queue across all producers. For convenience, individual classes still inherit a mixin that provides listen() and trigger() that delegate to the event manager.)
(请注意我是如何定义中央事件管理器的;那是因为我需要在所有生产者之间维护一个队列。为了方便起见,各个类仍然继承了一个 mixin,它提供了委托给事件管理器的 listen() 和 trigger()。)
Now it's possible to listen by doing:
现在可以通过执行以下操作来收听:
void SpaceshipController::create()
{
event_manager.listen(spaceship, "crash", shared_from_this(),
&SpaceshipController::on_crash);
}
void SpaceshipController::on_crash(CrashEvent* event)
{
// ...
}
That's pretty good, although it's verbose. I hate forcing every class to inherit enable_shared_from_this, and C++ requires that method references include the class name, which sucks, but both problems are probably unavoidable.
这很好,虽然它很冗长。我讨厌强制每个类都继承 enable_shared_from_this,而 C++ 要求方法引用包含类名,这很糟糕,但这两个问题可能都是不可避免的。
Unfortunately, I don't see how to implement listen() this way, since the classes are only known at compile time. I need to store the listeners in a per-producer map, which in turn contains a per-event-name map, something like:
不幸的是,我不知道如何以这种方式实现 listen(),因为这些类只在编译时才知道。我需要将侦听器存储在每个生产者映射中,该映射又包含一个每个事件名称映射,例如:
unordered_map<shared_ptr<ProducerClass>,
unordered_map<string, vector<boost:function1<void, EventClass*> > > > listeners;
But of course C++ doesn't let me. I could cheat:
但当然 C++ 不允许我这样做。我可以作弊:
unordered_map<shared_ptr<void*>,
unordered_map<string, vector<boost:function1<void, void*> > > > listeners;
but then that feels terribly dirty.
但那感觉非常脏。
So now I have to templatize EventManager or something, and keep one for each producer, perhaps? But I don't see how to do that without splitting up the queue, and I can't do that.
所以现在我必须模板化 EventManager 或其他东西,并为每个制作人保留一个,也许?但是我不知道如何在不拆分队列的情况下做到这一点,我也做不到。
Note how I am explicitly trying to avoid having to define pure interface classes for each type of event, Java-style:
请注意我是如何明确地避免为每种类型的事件定义纯接口类的,Java 风格:
class CrashEventListener
{
virtual void on_crash(CrashEvent* event) = 0;
}
With the number of events I have in mind, that would get awful, fast.
考虑到我想到的事件数量,这会变得很糟糕,很快。
It also raises another issue: I want to have fine-grained control over event handlers. For example, there are many producers that simply provide an event called "change". I want to be able to hook producer A's "change" event to on_a_change, and producer's B "change" event to on_b_change, for example. Per-event interfaces would make that inconvenient at best.
它还提出了另一个问题:我想对事件处理程序进行细粒度的控制。例如,有很多生产者只是提供一个叫做“change”的事件。例如,我希望能够将生产者 A 的“更改”事件连接到 on_a_change,将生产者 B 的“更改”事件连接到 on_b_change。每个事件的接口充其量只会让这变得不方便。
With all this in mind, could someone please point me in the right direction?
考虑到所有这些,有人可以指出我正确的方向吗?
回答by Stuart Berg
Update:This answer explains one option, but I think the modified version of this solution based on boost::any
is cleaner.
更新:此答案解释了一个选项,但我认为基于此解决方案的修改版本boost::any
更清晰。
First, let's imagine how the solution would look if you didn't need to queue up your events in an event manager. That is, let's imagine that all "spaceships" could signal the appropriate listener in real time whenever they have an event to report.
首先,让我们想象一下如果您不需要在事件管理器中对事件进行排队,解决方案会是什么样子。也就是说,让我们想象一下,只要有事件要报告,所有“宇宙飞船”都可以实时向适当的侦听器发出信号。
In that case, the simplest solution is to have each spaceship own several boost::signals, which listeners can connect to. When a ship wants to report an event, it simply fires the corresponding signal. (That is, call the signal via the operator() as though it were a function.)
在这种情况下,最简单的解决方案是让每艘飞船拥有多个 boost::signals,侦听器可以连接到这些信号。当一艘船想要报告一个事件时,它只需触发相应的信号。(也就是说,通过 operator() 调用信号,就好像它是一个函数一样。)
That system would hit a couple of your bullet-point requirements (consumers register themselves directly with the event-producers, and the handlers are just methods), but it doesn't solve the event queue problem. Fortunately, there's an easy fix for that.
该系统会满足您的几个要点要求(消费者直接向事件生产者注册自己,而处理程序只是方法),但它并没有解决事件队列问题。幸运的是,有一个简单的解决方法。
When an event producer (i.e. spaceship) wants to notify his listener(s) of an event, he shouldn't fire the signal himself. Instead, he should package up the signal invocation using boost::bind, and pass the resulting function object over to the event handler (in the form of a boost::function), which appends it to his queue. In this way, all events given to the event handler are merely of the following type: boost::function<void ()>
当事件生产者(即宇宙飞船)想要通知他的听众一个事件时,他不应该自己触发信号。相反,他应该使用 boost::bind 打包信号调用,并将结果函数对象传递给事件处理程序(以 boost::function 的形式),后者将其附加到他的队列中。这样,提供给事件处理程序的所有事件都只是以下类型:boost::function<void ()>
When it is time to flush the queue, the event handler merely calls all of the packaged events in his queue, each of which is essentially a callback that invokes the producer's (spaceship's) signal for a particular event.
当需要刷新队列时,事件处理程序仅调用其队列中的所有打包事件,每个事件本质上都是一个回调,为特定事件调用生产者(宇宙飞船)的信号。
Here's a complete sample implementation. The main() function demonstrates a simple "simulation" of the working system. I even threw some mutex locking in the event manager, since I assume he might be accessed by more than one thread. I didn't do the same for the controller or the spaceships. Obviously, the simple single-threaded test supplied in the main() function does not exercise the thread-safety of the event manager, but there's nothing complicated going on there.
这是一个完整的示例实现。main() 函数演示了对工作系统的简单“模拟”。我什至在事件管理器中抛出了一些互斥锁,因为我认为他可能会被多个线程访问。我没有为控制器或宇宙飞船做同样的事情。显然,main() 函数中提供的简单单线程测试并没有行使事件管理器的线程安全性,但是那里没有什么复杂的事情。
Finally, you'll notice that I included two different types of events. Two of the example events (crash and mutiny) expect to be calling methods with customized signatures (based on the type of information associated with that event). The other events (takeoff and landing) are "generic". Listeners provide a string (event name) when subscribing to generic events.
最后,您会注意到我包含了两种不同类型的事件。两个示例事件(崩溃和叛乱)期望调用具有自定义签名的方法(基于与该事件关联的信息类型)。其他事件(起飞和着陆)是“通用的”。订阅通用事件时,侦听器提供一个字符串(事件名称)。
In all, this implementation satisfies all of your bullet points. (With the generic event examples thrown in as a way to satisfy bullet point #2.) If you wanted to augment the "generic" signal type with an extra parameter for "EventInfo" or somesuch, that can easily be done.
总之,此实现满足您的所有要点。(使用通用事件示例作为满足第 2 点的一种方式。)如果您想使用“EventInfo”或其他类似参数的额外参数来扩充“通用”信号类型,这可以轻松完成。
Note that there's only one listener here (the controller), but nothing about the implementation restricts the number of listeners. You can add as many as you like. You will, however, have to make sure you manage the lifetime of your producers (spaceships) carefully.
请注意,这里只有一个侦听器(控制器),但关于实现并没有限制侦听器的数量。您可以根据需要添加任意数量。但是,您必须确保谨慎管理生产者(宇宙飞船)的生命周期。
One more thing: Since you expressed some disdain for having the spaceships inherit from enable_shared_from_this, I bound the spaceship object (via a weak_ptr) into the signal handler at the time of subscription. That way the spaceship doesn't have to explicitly provide the listener with a handle to himself when he fires the signal.
还有一件事:由于您对让飞船继承自 enable_shared_from_this 表示不屑,我在订阅时将飞船对象(通过weak_ptr)绑定到信号处理程序中。这样,当他发射信号时,宇宙飞船不必明确地向听众提供他自己的句柄。
By the way, the BEGIN/END Orbit output statements in main() are just there to show you that the events are not being received by the listeners until the event manager is triggered.
顺便说一下,main() 中的 BEGIN/END Orbit 输出语句只是为了向您展示在事件管理器被触发之前侦听器不会接收到事件。
(For reference: this compiles using gcc and boost 1.46, but should work with older versions of boost.)
(供参考:这使用 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;
}
When run, the above program produces the following output:
运行时,上述程序产生以下输出:
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
回答by Nicholas Frechette
This is almost a year later but there was no answer for this so here goes a different approach not relying on RTTI (which really shouldn't be required for this).
这几乎是一年之后,但没有答案,所以这里采用了一种不依赖 RTTI 的不同方法(这实际上不应该是必需的)。
- All events derive from a base event class that provides a virtual function to retrieve a UID
All events that derive from said class must have a macro present in the definition that implements some 'magic'
class EventFoo : public IEvent { public: IMPLEMENT_EVENT(EventFoo) // Regular EventFoo specific stuff };
The macro takes care to implement the virtual function mentioned above as well as implementing a static function returning the same 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(); }
Note that it is also trivial to support single event inheritance with this approach (the static unsigned char here only serves as a getto RTTI to avoid compiling with it enabled just for this)
Listeners implement a function of the form OnEvent(IEvent& _Event);
Listeners sprinkle some more macros in the definition to do the indirection
#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 */ } };
- 所有事件都派生自提供虚函数来检索 UID 的基事件类
从所述类派生的所有事件的定义中必须有一个宏,该宏实现了一些“魔法”
class EventFoo : public IEvent { public: IMPLEMENT_EVENT(EventFoo) // Regular EventFoo specific stuff };
宏负责实现上面提到的虚函数以及实现返回相同 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(); }
请注意,使用这种方法支持单事件继承也是微不足道的(这里的静态 unsigned char 仅用作 getto RTTI 以避免编译时为此启用它)
侦听器实现形式为 OnEvent(IEvent& _Event); 的函数。
侦听器在定义中添加更多宏来进行间接调用
#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 */ } };
Registering for events is fairly trivial since you only need to keep a list of IEventHandler* somewhere. OnEvent(..) becomes a giant switch/if-else mess but you are relieved from implementing it yourself. The declaration is also fairly clean using macros. You also always have the option of implementing OnEvent() yourself manually. Speed wise, I wouldn't worry too much. Performance will be very close to a switch statement for most compilers and unless you handle a lot of events in a single listener, it should still be very quick. You can also cache the UID value locally in the macro to avoid calling the virtual for every event type to handle in a listener. After the first virtual function call on the event, the vtable will be in the processor cache and any subsequent call will be very fast. The StaticGetUID functions will pretty much always get inlined in release builds to simply returning a constant. This ends up making the OnEvent code quite fast and compact.
注册事件相当简单,因为您只需要在某处保留 IEventHandler* 列表。OnEvent(..) 变成了一个巨大的 switch/if-else 混乱,但你可以从自己实现它中解脱出来。使用宏的声明也相当干净。您也始终可以选择自己手动实现 OnEvent()。速度明智,我不会太担心。对于大多数编译器,性能将非常接近 switch 语句,除非您在单个侦听器中处理大量事件,否则它应该仍然非常快。您还可以在宏中本地缓存 UID 值,以避免为要在侦听器中处理的每个事件类型调用虚拟。在事件的第一次虚函数调用之后,vtable 将在处理器缓存中,任何后续调用都将非常快。StaticGetUID 函数几乎总是会在发布版本中内联以简单地返回一个常量。这最终使 OnEvent 代码非常快速和紧凑。
The assembly is also very clean in x64 and powerpc (for the macro stub), not sure about x86. This makes stepping into the macro fairly painless if you really need to.
该程序集在 x64 和 powerpc(用于宏存根)中也非常干净,不确定 x86。如果您真的需要,这使得进入宏变得相当轻松。
This approach is typesafe at runtime since 2 events even with the same name, have different UIDs. Note that you could also use a hashing algorithm to generate the UID or some other method.
这种方法在运行时是类型安全的,因为 2 个事件即使具有相同的名称,也具有不同的 UID。请注意,您还可以使用散列算法来生成 UID 或其他一些方法。
回答by Stuart Berg
Okay, there's a reasonably simple solution to this that I was missing before. This is the way to go.
好的,有一个我以前缺少的相当简单的解决方案。这是要走的路。
Let me re-phrase the question and break it down into pieces that can be individually addressed.
让我重新表述这个问题,并将其分解为可以单独解决的部分。
I'm implementing a system in which "listeners" register themselves with event "producers". It's basically a standard "observer" pattern (a.k.a. "signals and slots"), but with a few twists.
In C++, what's the easiest way to manage the connections between my listeners and event producers?
我正在实施一个系统,其中“听众”向事件“生产者”注册自己。它基本上是一个标准的“观察者”模式(又名“信号和槽”),但有一些曲折。
在 C++ 中,管理侦听器和事件生成器之间连接的最简单方法是什么?
I recommend using an existing library for that. Either boost::signals or boost::signals2 will work nicely. Sure, you could roll your own signals and slots implementation, but why? boost::signals gives you a clean, tested, generic, and documented solution that many other c++ programmers will understand immediately when they look at your code.
我建议为此使用现有的库。boost::signals 或 boost::signals2 都可以很好地工作。当然,您可以推出自己的信号和插槽实现,但为什么呢?boost::signals 为您提供了一个干净的、经过测试的、通用的和文档化的解决方案,许多其他 C++ 程序员在查看您的代码时会立即理解。
Each of my producers is capable of producing several different types of events, which means that my listener functions will all have different signatures, right? Since the type of a boost::signal depends on the signatureof the function that handles, it, each producer will have to own several different typesof signals. I won't be able to put them in a collection (e.g. a map), which means each one will have to be declared separately. Even worse, I'll have to declare a separate "getter" function for every individual signal so that listeners can connect to it. Talk about boilerplate! How can I avoid that?
我的每个生产者都能够产生几种不同类型的事件,这意味着我的侦听器函数都将具有不同的签名,对吗?由于 boost::signal 的类型取决于处理函数的签名,因此每个生产者都必须拥有几种不同类型的信号。我将无法将它们放在一个集合中(例如地图),这意味着每个都必须单独声明。更糟糕的是,我必须为每个单独的信号声明一个单独的“getter”函数,以便听众可以连接到它。谈论样板!我怎样才能避免这种情况?
This is the tricky part.
这是棘手的部分。
As you mentioned in your question, one "solution" would be to have your signal emit the event as a void* type. And you're right: that's downright dirty. As my other answer to this question shows, there isa typesafe way to avoid hand-defining a separate signal for each event. If you go down that road, the compiler will catch any mistakes you make, but the code for that is kinda ugly.
正如您在问题中提到的,一个“解决方案”是让您的信号将事件作为 void* 类型发出。你是对的:那是彻头彻尾的肮脏。至于我的其他回答这个问题所显示的,是一个类型安全的方式,以避免手工定义每个事件单独的信号。如果你沿着这条路走下去,编译器会发现你犯的任何错误,但代码有点难看。
But that raises the question: Is it really so important to catch type errors at compile time? The problem with using the "dirty" void* trick is that you'll never know if you made a mistake until it's way too late. If you connect a handler to the wrong type of event, the behavior is undefined.
但这提出了一个问题:在编译时捕获类型错误真的那么重要吗?使用“脏” void* 技巧的问题在于,您永远不会知道自己是否犯了错误,直到为时已晚。如果将处理程序连接到错误类型的事件,则行为未定义。
Boost provides a library called boost::any
that solves this problem for us. It is conceptually similar to the void* approach, but lets you know if there's a problem. If you use boost::any, then all of your handlers will have the same function signature: void (const boost::any &)
. Sure, if you connect the wrong handler to a particular event, the compiler won't flag it for you. But you'll find out pretty quick when you test. That's because boost::any
throws an exception if you try to cast it to the wrong type. Your code will be free of tedious boilerplate, and no errors will go unnoticed (assuming your testing is reasonably complete).
Boost 提供了一个名为的库boost::any
为我们解决了这个问题。它在概念上类似于 void* 方法,但让您知道是否存在问题。如果您使用 boost::any,那么您的所有处理程序都将具有相同的函数签名:void (const boost::any &)
。当然,如果您将错误的处理程序连接到特定事件,编译器不会为您标记它。但是当你测试时你会很快发现。那是因为boost::any
如果您尝试将其强制转换为错误的类型,则会引发异常。您的代码将没有冗长乏味的样板,并且不会忽略任何错误(假设您的测试相当完整)。
Note: boost::any requires that you compile your code with RTTI turned on.
注意:boost::any 要求您在打开 RTTI 的情况下编译代码。
Okay, but there's a quirk to my system. I can't let the producers notify their listeners in real time. I need to somehow queue the events up and periodically flush the queue.
好的,但是我的系统有一个怪癖。我不能让制作人实时通知他们的听众。我需要以某种方式将事件排队并定期刷新队列。
The answer to this part is mostly independent of whatever system you choose for connecting your producers to your listeners. Just use boost::bind
to turn your event notification function into a "thunk" that can be executed by your event manager at some later time. Since all thunks have signature void ()
, it's easy to have your event manager hold a list of event notifications that are currently queued and waiting to be executed.
这部分的答案主要与您选择将制作人与听众联系起来的任何系统无关。只是boost::bind
用来将您的事件通知功能变成一个“ thunk”,可以在稍后由您的事件管理器执行。由于所有 thunk 都有签名void ()
,因此很容易让您的事件管理器保存当前排队等待执行的事件通知列表。
The following is a complete sample implementation using the techniques described above.
以下是使用上述技术的完整示例实现。
#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;
}
回答by Stuart Berg
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.
这是一个修改后的示例实现,它 (1) 在 Listener 实现类中需要更少的“样板”,并且 (2) 在事件管理器中排队事件时添加了稍微改进的调试信息。
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;
}
回答by adpalumbo
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.
您可以使用由所有侦听器实现的调度函数。EventManager 将为所有事件调用调度函数,然后侦听器可以决定如何在内部调度该事件。
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.
通过这种设计,EventManager 拥有排队和分派事件所需的所有信息(包括类型),并且您不会担心使用 java 模型时会出现接口方法爆炸。每个侦听器类只是on_event
为他们想要侦听的事件类型以及他们想要如何侦听它们适当地实现他们的调度方法。
回答by Emilio Garavaglia
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
调度程序是一个函子,它接受一个 event& 并遍历多态内部桥的集合(通常是一个 std::list),例如
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.
现在,您可以将 a 包装std::list<bridge*>
在一个类中,在“listen”(实际上template<class T, class E>void listen(T& t, bool(T::*mfn)(E&)
)上添加桥接器,并remove_if
使用调用same_as
. 该包装器也是一个接受事件的函子,在列表上迭代调用on_ev
,如果返回 true 则最终中断循环。
Every time i tried to avoid the dynamic_cast
-s i found in fact myself in trying to re-implement it through type-tags etc. so ... for runtime solution. let RTTI to play its role.
每次我试图避免dynamic_cast
-si 实际上是我自己在尝试通过类型标签等重新实现它时发现的,所以......对于运行时解决方案。让RTTI发挥作用。
回答by spraff
Qt's event model is instructive
Qt 的事件模型很有启发性
- there is a central thread which dispatches events
- you can post events to it synchronouslyor 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 handlerinto a slave object to filter and/or transform the slave's events
- 有一个调度事件的中央线程
- 您可以同步或异步地向其发布事件
- 可继承的事件处理程序对应于基类的继承树
Event
- 所有事件处理程序都是虚拟的
- 每个事件处理程序定义它是否处理事件
- 对象可以将自己的事件处理程序注入从属对象以过滤和/或转换从属对象的事件