我试图在 C++ 中设计一个通用的(但有点特定于用例的)事件传递机制,而不违背“新风格”C++ 的原则,同时又不过度使用模板。
我的用例有些特殊,因为我需要完全控制事件的分发时间。事件系统是世界模拟的基础,其中世界的每次迭代都作用于前一帧生成的事件。所以我要求所有事件在分派(dispatch)之前先排队,以便应用程序可以在特定的时间间隔刷新队列,有点像经典的 GUI 事件循环。
我的用例在 Ruby、Python 甚至 C 中实现起来很简单,但是使用 C++ 我来的有点短。我看过 Boost::Signal 和其他类似的库,但它们似乎太复杂或不灵活,无法适应我的特定用例。 (尤其是 Boost,它是基于模板的,常常到了完全困惑的地步,尤其是像 boost::bind 或 boost::function 这样的东西。)
这是系统,大致如下:
这是消费者的“理想”伪代码示例:
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++,要么必须使用具有编译时定义类型的模板级多态性。
我已经尝试过使用 boost::bind(),我可以生成一个这样的监听方法:
class EventManager
{
template <class ProducerClass, class ListenerClass, class EventClass>
void EventManager::listen(
shared_ptr<ProducerClass> producer,
string event_name,
shared_ptr<ListenerClass> listener,
void (ListenerClass::*method)(EventClass* event)
)
{
boost::function1<void, EventClass*> bound_method = boost::bind(method, listener, _1);
// ... add handler to a map for later execution ...
}
}
(请注意我是如何定义中央事件管理器的;那是因为我需要在所有生产者之间维护一个队列。为了方便起见,各个类仍然继承了一个 mixin,它提供了委托(delegate)给事件管理器的 listen() 和 trigger()。)
现在可以通过执行以下操作来收听:
void SpaceshipController::create()
{
event_manager.listen(spaceship, "crash", shared_from_this(),
&SpaceshipController::on_crash);
}
void SpaceshipController::on_crash(CrashEvent* event)
{
// ...
}
这很好,虽然它很冗长。我讨厌强制每个类都继承 enable_shared_from_this,而 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 或其他东西,并为每个制作人保留一个,也许?但是我不知道如何在不拆分队列的情况下做到这一点,我也做不到。
请注意我是如何明确地避免为每种类型的事件定义纯接口(interface)类的,Java 风格:
class CrashEventListener
{
virtual void on_crash(CrashEvent* event) = 0;
}
考虑到我想到的事件数量,这会变得很糟糕,很快。
它还提出了另一个问题:我想对事件处理程序进行细粒度的控制。例如,有很多生产者只是提供一个叫做“change”的事件。例如,我希望能够将生产者 A 的“更改”事件连接到 on_a_change,将生产者 B 的“更改”事件连接到 on_b_change。每个事件的接口(interface)充其量只会让这变得不方便。
考虑到所有这些,有人可以指出我正确的方向吗?
最佳答案
更新:这个答案解释了一种选择,但我认为 the modified version of this solution based on boost::any
更干净。
首先,让我们想象一下如果您不需要在事件管理器中对事件进行排队,解决方案会是什么样子。也就是说,让我们想象一下,只要有事件要报告,所有“宇宙飞船”都可以实时向适当的监听器发出信号。
在这种情况下,最简单的解决方案是让每艘飞船拥有多个 boost::signals,监听器可以连接到这些信号。当一艘船想要报告一个事件时,它只需触发相应的信号。 (也就是说,通过 operator() 调用信号,就好像它是一个函数一样。)
该系统会满足您的几个要点要求(消费者直接向事件生产者注册自己,而处理程序只是方法),但它并没有解决事件队列问题。幸运的是,有一个简单的解决方法。
当事件生产者(即宇宙飞船)想要通知他的听众一个事件时,他不应该自己触发信号。相反,他应该使用 boost::bind 打包信号调用,并将结果函数对象传递给事件处理程序(以 boost::function 的形式),后者将其附加到他的队列中。这样,所有提供给事件处理程序的事件都只是以下类型:boost::function<void ()>
当需要刷新队列时,事件处理程序仅调用其队列中的所有打包事件,每个事件本质上都是一个回调,为特定事件调用生产者(宇宙飞船)的信号。
这是一个完整的示例实现。 main() 函数演示了对工作系统的简单“模拟”。我什至在事件管理器中抛出了一些互斥锁,因为我认为他可能会被多个线程访问。我没有为 Controller 或宇宙飞船做同样的事情。显然,main() 函数中提供的简单单线程测试并没有行使事件管理器的线程安全性,但是那里没有什么复杂的事情。
最后,您会注意到我包含了两种不同类型的事件。两个示例事件(崩溃和叛乱)期望调用具有自定义签名的方法(基于与该事件关联的信息类型)。其他事件(起飞和着陆)是“通用的”。订阅通用事件时,监听器提供一个字符串(事件名称)。
总之,此实现满足您的所有要点。 (使用通用事件示例作为满足第 2 点的一种方式。)如果您想使用“EventInfo”或其他类似参数的额外参数来扩充“通用”信号类型,这可以轻松完成。
请注意,这里只有一个监听器( Controller ),但实现没有限制监听器的数量。您可以根据需要添加任意数量。但是,您必须确保谨慎管理生产者(宇宙飞船)的生命周期。
还有一件事:由于您对让飞船继承自 enable_shared_from_this 表示不屑,我在订阅时将飞船对象(通过weak_ptr)绑定(bind)到信号处理程序中。这样,当他发射信号时,宇宙飞船不必明确地向听众提供他自己的句柄。
顺便说一下,main() 中的 BEGIN/END Orbit 输出语句只是为了向您展示在事件管理器被触发之前监听器不会接收到事件。
(供引用:这使用 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
关于c++ - 用 C++ 设计事件机制,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7464025/