在C++中设计事件机制

Ale*_*ubo 41 c++

我试图在C++中设计一个通用的(但有些特定于用例的)事件传递机制,而不会违反"新风格"C++,并且同时不会过度使用模板.

我的用例有些特别之处在于我需要完全控制何时分发事件.事件系统是世界模拟的基础,世界的每次迭代都对前一帧生成的事件起作用.因此,我要求所有事件在调度之前排队,以便应用程序可以按特定时间间隔刷新队列,有点像经典的GUI事件循环.

我的用例在Ruby,Python甚至C中实现都很简单,但是使用C++我的内容有点短.我已经看了的boost ::信号和其它类似的库,但他们似乎过于复杂或不灵活,以适应我特别的使用情况.(特别是,Boost经常被模板化到完全混乱的程度,特别是像boost :: bind或boost :: function这样的东西.)


这是系统,大致如下:

  • 消费者通过直接向生成事件的对象注册自己来监听事件.

  • 事件只是字符串名称,但每个事件可能附加了附加数据.

  • 听众只是方法.如果这是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);
}
Run Code Online (Sandbox Code Playgroud)

这是一个制片人:

Spaceship::collided_with(CollisionObject object) {
    trigger("crash", new CrashEvent(object));
}
Run Code Online (Sandbox Code Playgroud)

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


问题是,任何一个人都必须使用旧式C++来投射多态实例和丑陋,或者必须使用模板级多态与编译时定义的类型.

我已尝试使用boost :: bind(),我可以生成这样的listen方法:

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 ...
    }
}
Run Code Online (Sandbox Code Playgroud)

(注意我是如何定义中央事件管理器的;这是因为我需要在所有生成器中维护一个队列.为方便起见,各个类仍然继承了一个mixin,它提供了委托给事件管理器的listen()和trigger().)

现在可以通过这样做来倾听:

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

void SpaceshipController::on_crash(CrashEvent* event)
{
    // ...
}
Run Code Online (Sandbox Code Playgroud)

这很好,虽然它很冗长.我讨厌强制每个类继承enable_shared_from_this,而C++要求方法引用包括类名,这很糟糕,但这两个问题都可能是不可避免的.

不幸的是,我没有看到如何以这种方式实现listen(),因为这些类只在编译时才知道.我需要将监听器存储在每个生成器映射中,而映射器又包含每个事件名称映射,例如:

unordered_map<shared_ptr<ProducerClass>,
    unordered_map<string, vector<boost:function1<void, EventClass*> > > > listeners;
Run Code Online (Sandbox Code Playgroud)

但当然C++不允许我.我可以作弊:

unordered_map<shared_ptr<void*>,
    unordered_map<string, vector<boost:function1<void, void*> > > > listeners;
Run Code Online (Sandbox Code Playgroud)

但那时感觉非常脏.

所以现在我必须对EventManager或其他东西进行模板化,并为每个制作人保留一个,或许?但是我没有看到如何在不拆分队列的情况下做到这一点,我不能那样做.


请注意我是如何明确地避免为每种类型的事件定义纯接口类,Java风格:

class CrashEventListener
{
    virtual void on_crash(CrashEvent* event) = 0;
}
Run Code Online (Sandbox Code Playgroud)

随着我想到的事件数量的增加,这将变得非常糟糕,快速.

它还提出了另一个问题:我希望对事件处理程序进行细粒度控制.例如,有许多生产者只提供一个名为"改变"的事件.我希望能够将生产者A的"更改"事件挂钩到on_a_change,并将生产者的B"更改"事件挂钩到on_b_change,例如.每事件接口最多会造成不便.


考虑到所有这些,有人可以指出我正确的方向吗?

Stu*_*erg 27

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


首先,让我们假设如果您不需要在事件管理器中排队事件,解决方案将如何显示.也就是说,让我们想象一下,只要有事件报告,所有"宇宙飞船"都可以实时向适当的听众发出信号.

在这种情况下,最简单的解决方案是让每个宇宙飞船拥有几个boost :: signal,侦听器可以连接到这些信号.当船舶想要报告事件时,它只会触发相应的信号.(也就是说,通过operator()调用信号,就像它是一个函数一样.)

该系统将达到您的几个要点(消费者直接向事件生成者注册,而处理程序只是方法),但它不能解决事件队列问题.幸运的是,有一个简单的解决方案.

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

当需要刷新队列时,事件处理程序仅调用其队列中的所有打包事件,每个事件本质上都是一个回调,用于调用特定事件的生产者(飞船)信号.

这是一个完整的示例实现.main()函数演示了工作系统的简单"模拟".我甚至在事件管理器中抛出了一些互斥锁,因为我认为他可能被多个线程访问.我对控制器或宇宙飞船没有做同样的事情.显然,main()函数中提供的简单单线程测试不会影响事件管理器的线程安全性,但是没有什么复杂的事情发生在那里.

最后,您会注意到我包含了两种不同类型的事件.两个示例事件(崩溃和叛变)期望调用具有自定义签名的方法(基于与该事件相关联的信息的类型).其他事件(起飞和着陆)是"通用的".订阅通用事件时,监听器提供字符串(事件名称).

总之,此实现满足您的所有要点.(将通用事件示例作为满足项目符号#2的方式引入.)如果您想使用"EventInfo"的额外参数来扩充"通用"信号类型,或者某些事情可以轻松完成.

请注意,这里只有一个侦听器(控制器),但实现没有限制侦听器的数量.您可以添加任意数量的内容.但是,您必须确保仔细管理生产者的生命周期(宇宙飞船).

还有一件事:既然你对太空飞船继承自enable_shared_from_this表示了一些蔑视,我在订阅时将太空船对象(通过weak_ptr)绑定到信号处理程序中.这样,宇宙飞船在发射信号时不必明确地为听众提供一个自己的手柄.

顺便说一下,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;
}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)


Nic*_*tte 6

这差不多一年之后但是没有答案,所以这里有一种不依赖于RTTI的不同方法(实际上这不应该是必需的).

  • 所有事件都派生自基本事件类,该类提供用于检索UID的虚函数
  • 从所述类派生的所有事件必须在定义中存在一个实现某些"魔法"的宏

    class EventFoo : public IEvent
    {
    public:
        IMPLEMENT_EVENT(EventFoo)
        // Regular EventFoo specific stuff
    };
    
    Run Code Online (Sandbox Code Playgroud)
  • 宏负责实现上面提到的虚函数以及实现返回相同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(); }
    
    Run Code Online (Sandbox Code Playgroud)
  • 请注意,使用这种方法支持单个事件继承也很简单(这里的静态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 */ }
    };
    
    Run Code Online (Sandbox Code Playgroud)

注册事件是相当简单的,因为您只需要在某处保留IEventHandler*的列表.OnEvent(..)成为一个巨大的转换/如果其他一塌糊涂,但你可以放心自己实现它.声明使用宏也相当干净.您也可以选择手动实现OnEvent().速度明智,我不会太担心.对于大多数编译器而言,性能将非常接近switch语句,除非您在单个侦听器中处理大量事件,否则它应该非常快.您还可以在宏中本地缓存UID值,以避免为侦听器中的每个事件类型调用虚拟对象.在事件的第一个虚函数调用之后,vtable将在处理器缓存中,并且任何后续调用将非常快.StaticGetUID函数几乎总是在发布版本中内联,只返回一个常量.这最终使OnEvent代码非常快速和紧凑.

在x64和powerpc(对于宏存根)中,程序集也非常干净,不确定x86.如果你真的需要,这使得进入宏非常轻松.

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


Stu*_*erg 5

更新:这个答案仍然优于这里的顶级答案(也来自我)。但是使用 C++11,它可以做得更好:

  • 几乎所有使用boost都可以替换为 C++11 特性或std库。(只有signals2必须留下。)
  • 现在any不再需要 RTTI。

好的,有一个我以前缺少的相当简单的解决方案。这是要走的路。

让我重新表述这个问题,并将其分解为可以单独解决的部分。

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

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

我建议为此使用现有的库。boost::signals 或 boost::signals2 都可以很好地工作。当然,您可以推出自己的信号和插槽实现,但为什么呢?boost::signals 为您提供了一个干净的、经过测试的、通用的和文档化的解决方案,许多其他 C++ 程序员在查看您的代码时会立即理解。

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

这是棘手的部分。

正如您在问题中提到的,一个“解决方案”是让您的信号将事件作为 void* 类型发出。你是对的:那是彻头彻尾的肮脏。至于我的其他回答这个问题所显示的,一个类型安全的方式,以避免手工定义每个事件单独的信号。如果你沿着这条路走下去,编译器会发现你犯的任何错误,但代码有点难看。

但这提出了一个问题:在编译时捕获类型错误真的那么重要吗?使用“脏” void* 技巧的问题在于,您永远不会知道自己是否犯了错误,直到为时已晚。如果将处理程序连接到错误类型的事件,则行为未定义。

Boost 提供了一个名为的库boost::any为我们解决了这个问题。它在概念上类似于 void* 方法,但让您知道是否存在问题。如果您使用 boost::any,那么您的所有处理程序都将具有相同的函数签名:void (const boost::any &)。当然,如果您将错误的处理程序连接到特定事件,编译器不会为您标记它。但是当你测试时你会很快发现。那是因为boost::any如果您尝试将其强制转换为错误的类型,则会引发异常。您的代码将没有冗长乏味的样板,并且不会忽略任何错误(假设您的测试相当完整)。

注意:boost::any 要求您在打开 RTTI 的情况下编译代码。[编辑: boost::any不再需要 RTTI。 一样std::any。]

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

这部分的答案主要与您选择将制作人与听众联系起来的任何系统无关。只是boost::bind用来将您的事件通知功能变成一个“ thunk ”,可以在稍后由您的事件管理器执行。由于所有 thunk 都有签名void (),因此很容易让您的事件管理器保存当前排队等待执行的事件通知列表。

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

#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;
}
Run Code Online (Sandbox Code Playgroud)


adp*_*mbo 2

您可以使用由所有侦听器实现的调度函数。EventManager 将为所有事件调用调度函数,然后侦听器可以决定如何在内部调度该事件。

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

那么你的监听函数将如下所示:

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

通过这种设计,EventManager 拥有对事件进行排队和分派所需的所有信息(包括类型),并且您不必担心 java 模型中的接口方法爆炸。每个监听器类只是on_event根据它们想要监听的事件类型以及它们想要监听的方式适当地实现它们的调度方法。