在C ++中设计事件机制

时间:2011-09-18 19:51:08

标签: c++

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

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

我的用例在Ruby,Python甚至C中实现都很简单,但是使用C ++我的内容有点短。我已经查看了Boost :: Signal和其他类似的库,但它们看起来太复杂或不灵活,不适合我的特定用例。 (特别是,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);
}

这是制作人:

Spaceship::collided_with(CollisionObject object) {
    trigger("crash", new CrashEvent(object));
}

这一切都很好,但转换成现代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 ...
    }
}

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

现在可以通过以下方式倾听:

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

void SpaceshipController::on_crash(CrashEvent* event)
{
    // ...
}

这很不错,虽然它很冗长。我讨厌强制每个类继承enable_shared_from_this,并且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或其他东西进行模板化,并为每个制作人保留一个,或许?但是我没有看到如何在不拆分队列的情况下做到这一点,我不能那样做。


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

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

根据我想到的事件数量,这会很快,很快。

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


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

7 个答案:

答案 0 :(得分:27)

更新:这个答案解释了一个选项,但我认为the modified version of this solution based on 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;
}

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

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

答案 1 :(得分:6)

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

  • 所有事件都派生自一个基本事件类,该类提供了一个用于检索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&amp; _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 */ }
    };
    

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

在x64和powerpc(对于宏存根)中,程序集也很干净,不确定x86。如果你真的需要,这可以毫不费力地进入宏。

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

答案 2 :(得分:4)

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

让我重新说出这个问题并将其分解为可以单独解决的问题。

  

我正在实施一个“听众”注册的系统   他们自己与事件“生产者”。它基本上是一个标准   “observer”模式(a.k.a。“信号和插槽”),但有一些   扭转。

     

在C ++中,管理连接的最简单方法是什么   在我的听众和活动制作人之间?

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

  

我的每个制作人都能够制作几种不同的类型   事件,这意味着我的听众功能都将拥有   不同的签名,对吗?由于boost :: signal的类型   取决于处理它的函数的签名,每个   制作人必须拥有几种不同类型的信号。一世   将无法将它们放入集合(例如地图)中,这意味着每个集合   一个人必须单独宣布。更糟糕的是,我必须这样做   为每个单独的信号声明一个单独的“getter”函数   听众可以连接到它。谈论样板!我该如何避免

这是棘手的部分。

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

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

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

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

  

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

这部分的答案主要与您选择将制作人与听众连接的系统无关。只需使用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;
}

答案 3 :(得分:2)

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

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

然后你的听力功能看起来像:

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

通过这种设计,EventManager具有排队和分派事件所需的所有信息(包括类型),并且您没有使用java模型担心的接口方法爆炸。每个监听器类只是适当地实现他们的on_event调度方法,用于他们想要监听的事件类型以及他们想要如何监听它们。

答案 4 :(得分:2)

这是一个经过修改的示例实现,(1)在Listener实现类中需要较少的“样板”,(2)在事件管理器中排队事件时添加稍微改进的调试信息。

棘手的是让制作人拥有不同类型的多个信号,但使用单一功能来访问它们。在这个实现中,我使用多重继承来实现这一点。是的,是的,我知道:它是邪恶的等等。它也恰好在这种情况下起作用。

#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;
}

答案 5 :(得分:1)

我总是发现的最简单的方法是类似的情况是来自多态空基(一个只有虚拟结构的类)的所有事件,每个事件都是一个带有事件参数的类:

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

调度员是一个参加活动的探险家。并遍历一个多态内部桥梁的集合(通常是一个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;
    }
};

现在,您可以将std::list<bridge*>包含在一个类中,在“listen”(实际上为template<class T, class E>void listen(T& t, bool(T::*mfn)(E&))上添加桥接,并通过remove_if删除unist,并使用谓词调用same_as }。 该包装器也是一个接受事件的函子,在调用on_ev的列表上进行迭代,如果返回true则最终打破循环。

每次我试图避免dynamic_cast - 我实际上发现自己试图通过类型标签等重新实现它所以...对于运行时解决方案。让RTTI发挥作用。

答案 6 :(得分:1)

Qt的活动模式很有启发性