45

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.

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.

My use case is trivial to implement in Ruby, Python or even C, but with C++ I am coming up a bit short. I have looked 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.)


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 next iteration -- so there are really two queues internally.


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.


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.

I have experimented with using boost::bind(), and I can produce a listen method like this:

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.)

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.

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:

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:

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.


Note how I am explicitly trying to avoid having to define pure interface classes for each type of event, Java-style:

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.


With all this in mind, could someone please point me in the right direction?

7 Answers 7

28

Update: This answer explains one option, but I think the modified version of this solution based on boost::any is cleaner.


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.)

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 ()>

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.

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.

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.

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.

(For reference: this compiles using gcc and boost 1.46, but should work with older versions of 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
Sign up to request clarification or add additional context in comments.

13 Comments

I'm going to have to spend time reading and sorting through the logic since I'm not particularly familiar with boost::signals, but this looks usable, actually. Kudos for supplying a working example, that's way beyond what I was hoping for. Wow! :-)
That code has a few warts, but it should be manageable. In particular, it's a little ugly that you can't store the generic signals directly in a map (since signals are noncopyable). By the way, I used boost::signals2 here, but it would work just as well with the original boost::signals library (I'm not doing anything fancy).
All right, I have read through it and think I understand how everything fits together. There are a few problems; perhaps they can be fixed? (Due to comment limit I will have to split this up.) (1) Having consumers maintain their subscriptions is simply out of the question. Too much boilerplate which I do not want to saddle the implementation classes with. This has to be maintained by the event manager; I suspect that's fairly trivial.
(2) Providing methods for the signals (like subscribeToCrashEvents) is too much work -- too much to saddle the implementation classes with. Even having to implement getter methods for each signal is simply out of the question (and remember, no macros, please!). In other words, it looks like the signals would have to be declared as public members. Ew! (3) The syntax boost::bind(&my_handler, this, other, _1, _2) to assign signals is really, really awful. It's unreadable, ugly and weird. I don't want to see that anywhere in the implementation classes.
And finally: (4) It looks hard/impossible to debug the flow of events. Looks like the event manager can't log the name of each event is it's propagated, for example, since signals are nameless.
|
7

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).

  • 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 */ }
    };
    

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.

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.

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.

4 Comments

Interesting approach. I prefer to write code without macros myself, but I can see why some people might choose this kind of solution.
This is a simple solution but scales very badly. The cost of event dispatching increases rapidly with the total number of subscribers (if you have one thousand subscribers, each event dispatch costs one thousand virtual function calls, even if only one object is interested in the event).
The macro can be eliminated by making IEvent a template and using the curiously recurring template parameter pattern. That will instantiate different functions for StaticGetUID containing different static chars, and thus returning different values, no macro involved.
As always, YMMV. A system like this isn't conceived for hundred of bound events on a single class type, I used it in the past with success when the amount of events fired was low and the number of bound handlers was low as well. The template VS macro approach depends a lot on your use case. Using a template approach might need a base interface anyway as well to manipulate raw IEvents, so you need to introduce some intermediate EventBase template.
6

Update: This answer is still superior to the top-rated answer here (also from me). But using C++11, it could be made even better:

  • Nearly all uses of boost can be replaced with C++11 features or the std library. (Only signals2 must remain.)
  • Now any no longer requires RTTI.

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?

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.

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 signature of the function that handles, it, each producer will have to own several different types of 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?

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 is a 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.

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.

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).

Note: boost::any requires that you compile your code with RTTI turned on. [Edit: boost::any no longer requires RTTI. Same for std::any.]

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.

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

Comments

2

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

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

Then your listen function would look like:

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

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

2 Comments

Although that would work, it would force me to implement all event handling as a big switch statement, which I don't care much for. It's inefficient, for one; perhaps the compiler will be able to optimize it as an efficient jump table, but I'm not sure I can count on it. It also means I have to accept an event object that points to a base (Event*) that I would then have to cast into the right event class (CrashEvent*).
…Also, switch statements don't support strings, so I would have to invent constants for every event. Since I want the system to be completely modular (ie. extensible by separate modules), I can't use an enum type declared centrally (enum { kCrashEvent, ... } EventType) since the event namespace must be extensible. So the only way to enforce type safety for the event type would be to create an EventType class that new event types inherit; that feels really dirty, not to mention memory-inefficient.
2

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

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

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

struct bridge
{
    virtual ~bridge() {}
    virtual bool same_as(const bridge* p) const=0; //to implement unlisten
    virtual bool on_ev(event& ev)=0;
};

template<class E, class T>
struct fnbridge: public bridge
{
    T* pt;
    bool(T::*mfn)(E&);

    virtual bool on_ev(event& ev)
    {
        E* pe = dynamic_cast<E*>(&ev);
        return pe && (pt->*mfn)(*pe);
    }

    virtual bool same_as(const bridge* p)
    {
        const fnbridge* pb = dynamic_cast<const fnbridge*>(p);
        return pb && pb->pt == pt && pb->mfn == mfn;
    }
};

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

Every time i tried to avoid the dynamic_cast-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.

1 Comment

I had no idea you could subclass a non-template struct/class using a template. That might very well solve the "polymorphic collection" problem I had. Thanks!
2

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

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

#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <map>

#include <boost/bind.hpp>
#include <boost/function.hpp>
#include <boost/signals2.hpp>
#include <boost/foreach.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/static_assert.hpp>

// Forward declarations
class Spaceship;
typedef boost::shared_ptr<Spaceship> SpaceshipPtr;
typedef boost::weak_ptr<Spaceship> SpaceshipWPtr;

class EventManager;
typedef boost::shared_ptr<EventManager> EventManagerPtr;

// ******************************************************************
// EVENT DEFINITIONS
// ******************************************************************
struct TakeoffEvent
{
    static const std::string name ;
};
const std::string TakeoffEvent::name = "takeoff" ;

struct LandingEvent
{
    static const std::string name ;
};
const std::string LandingEvent::name = "landing" ;

struct CrashEvent
{
    static const std::string name ;

    CrashEvent(const std::string & s)
        : sound(s) {}

    std::string sound ;
};
const std::string CrashEvent::name = "crash" ;

struct MutinyEvent
{
    static const std::string name ;

    MutinyEvent(bool s, int n)
        : successful(s)
        , numDead(n) {}

    bool successful ;
    int numDead ;
};
const std::string MutinyEvent::name = "mutiny" ;
// ******************************************************************
// ******************************************************************

class EventManager
{
public:
    // Notify listeners of all recent events
    void FlushAllQueuedEvents()
    {
        NotificationVec vecNotifications;

        // Open a protected scope to modify the notification list
        {
            // One thread at a time
            boost::recursive_mutex::scoped_lock lock( m_notificationProtection );

            // Copy the notification vector to our local list and clear it at the same time
            std::swap( vecNotifications, m_vecQueuedNotifications );
        }

        // Now loop over the notification callbacks and call each one.
        // Since we're looping over the copy we just made, new events won't affect us.
        BOOST_FOREACH( const NamedNotification & nameAndFn, vecNotifications )
        {
            // Debug output
            std::cout << "Flushing " << nameAndFn.first << std::endl ;

            // call the listener(s)
            nameAndFn.second() ;
        }
    }

    // Callback signature
    typedef void EventNotificationFnSignature();
    typedef boost::function<EventNotificationFnSignature> EventNotificationFn;

    //! Queue an event with the event manager
    void QueueEvent( const std::string & name, const EventNotificationFn & nameAndEvent )
    {
        // One thread at a time.
        boost::recursive_mutex::scoped_lock lock(  m_notificationProtection );

        m_vecQueuedNotifications.push_back( NamedNotification(name, nameAndEvent) );
    }

private:
    // Queue of events
    typedef std::pair<std::string, EventNotificationFn> NamedNotification ;
    typedef std::vector<NamedNotification> NotificationVec ;
    NotificationVec m_vecQueuedNotifications;

    // This mutex is used to ensure one-at-a-time access to the list of notifications
    boost::recursive_mutex m_notificationProtection ;
};

template <typename _Event>
class Producer
{
public:
    Producer( const EventManagerPtr & pEventManager )
        : m_pEventManager(pEventManager) {}

    typedef void SignalSignature(const _Event &) ;

    boost::signals2::connection subscribe( const boost::function<SignalSignature> & fn )
    {
        return m_signal.connect(fn) ;
    }

    void trigger(const _Event & event)
    {
        m_pEventManager->QueueEvent( event.name, boost::bind( boost::ref(m_signal), event ) ) ;
    }

protected:
    // Instantiate the tuple of signals
    boost::signals2::signal<SignalSignature> m_signal ;
    EventManagerPtr m_pEventManager ;
};

class Spaceship : public Producer<TakeoffEvent>
                , public Producer<LandingEvent>
                , public Producer<CrashEvent>
                , public Producer<MutinyEvent>
{
public:

    Spaceship(const std::string & name, const EventManagerPtr & pEventManager)
        : Producer<TakeoffEvent>(pEventManager)
        , Producer<LandingEvent>(pEventManager)
        , Producer<CrashEvent>(pEventManager)
        , Producer<MutinyEvent>(pEventManager)
        , m_name(name)
    {
    }

    std::string & name()
    {
        return m_name ;
    }

    template <typename _Event>
    boost::signals2::connection subscribe( const boost::function<void (const _Event &)> & fn )
    {
        // call the correct base class
        return Producer<_Event>::subscribe( fn ) ;
    }

    template <typename _Event>
    void trigger(const _Event & event = _Event() )
    {
        // call the correct base class
        Producer<_Event>::trigger(event) ;
    }

private:

    std::string m_name;
};

class Listener
{
public:
    Listener( const std::set<SpaceshipPtr> & ships )
    {
        // For every ship, subscribe to all of the events we're interested in.
        BOOST_FOREACH( const SpaceshipPtr & pSpaceship, ships )
        {
            m_ships.insert( pSpaceship );

            // Bind up a weak_ptr in the handler calls (using a shared_ptr would cause a memory leak)
            SpaceshipWPtr wpSpaceship(pSpaceship);

            // Register event callback functions with the spaceship so he can notify us.
            // Bind a pointer to the particular spaceship so we know who originated the event.
            m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<CrashEvent>(
               boost::bind( &Listener::HandleCrashEvent, this, wpSpaceship, _1 ) ) );

            m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<MutinyEvent>(
               boost::bind( &Listener::HandleMutinyEvent, this, wpSpaceship, _1 ) ) );

            m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<TakeoffEvent>(
               boost::bind( &Listener::HandleGenericEvent<TakeoffEvent>, this, wpSpaceship, _1 ) ) );

            m_allConnections[pSpaceship].push_back( pSpaceship->subscribe<LandingEvent>(
               boost::bind( &Listener::HandleGenericEvent<LandingEvent>, this, wpSpaceship, _1 ) ) );
        }
    }

    ~Listener()
    {
        // Disconnect from any signals we still have
        BOOST_FOREACH( const SpaceshipPtr pShip, m_ships )
        {
            BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pShip] )
            {
                conn.disconnect();
            }
        }
    }

private:
    typedef std::vector<boost::signals2::connection> ConnectionVec;
    std::map<SpaceshipPtr, ConnectionVec> m_allConnections;
    std::set<SpaceshipPtr> m_ships;

    template <typename _Event>
    void HandleGenericEvent( SpaceshipWPtr wpSpaceship, const _Event & event)
    {
        // Obtain a shared ptr from the weak ptr
        SpaceshipPtr pSpaceship = wpSpaceship.lock();

        std::cout << "Event on " << pSpaceship->name() << ": " << _Event::name << '\n';
    }

    void HandleCrashEvent(SpaceshipWPtr wpSpaceship, const CrashEvent & crash)
    {
        // Obtain a shared ptr from the weak ptr
        SpaceshipPtr pSpaceship = wpSpaceship.lock();

        std::cout << pSpaceship->name() << " crashed with sound: " << crash.sound << '\n';

        // That ship is dead.  Delete it from the list of ships we track.
        m_ships.erase(pSpaceship);

        // Also, make sure we don't get any more events from it
        BOOST_FOREACH( boost::signals2::connection & conn, m_allConnections[pSpaceship] )
        {
            conn.disconnect();
        }
        m_allConnections.erase(pSpaceship);
    }

    void HandleMutinyEvent(SpaceshipWPtr wpSpaceship, const MutinyEvent & mutiny )
    {
        SpaceshipPtr pSpaceship = wpSpaceship.lock();
        std::cout << (mutiny.successful ? "Successful" : "Unsuccessful" ) ;
        std::cout << " mutiny on " << pSpaceship->name() << "! (" << mutiny.numDead << " dead crew members)\n";
    }
};

int main()
{
    // Instantiate an event manager
    EventManagerPtr pEventManager( new EventManager );

    // Create some ships to play with
    int numShips = 5;
    std::vector<SpaceshipPtr> vecShips;
    for (int shipIndex = 0; shipIndex < numShips; ++shipIndex)
    {
        std::string name = "Ship #" + boost::lexical_cast<std::string>(shipIndex);
        SpaceshipPtr pSpaceship( new Spaceship(name, pEventManager) );
        vecShips.push_back(pSpaceship);
    }

    // Create the controller with our ships
    std::set<SpaceshipPtr> setShips( vecShips.begin(), vecShips.end() );
    Listener controller(setShips);

    // Quick-and-dirty "simulation"
    // We'll cause various events to happen to the ships in the simulation,
    // And periodically flush the events by triggering the event manager

    std::cout << "BEGIN Orbit #1" << std::endl;
    vecShips[0]->trigger( TakeoffEvent() );
    vecShips[0]->trigger( CrashEvent("Kaboom!") );
    vecShips[1]->trigger( TakeoffEvent() );
    vecShips[1]->trigger( CrashEvent("Blam!") );
    vecShips[2]->trigger( TakeoffEvent() );
    vecShips[2]->trigger( MutinyEvent(false, 7) );
    std::cout << "END Orbit #1\n" << std::endl;

    pEventManager->FlushAllQueuedEvents();
    std::cout << "\n" ;

    std::cout << "BEGIN Orbit #2" << std::endl;
    vecShips[3]->trigger( TakeoffEvent() );
    vecShips[3]->trigger( MutinyEvent(true, 2) );
    vecShips[4]->trigger( TakeoffEvent() );
    vecShips[4]->trigger( CrashEvent("Splat!") );
    std::cout << "END Orbit #2\n" << std::endl;

    pEventManager->FlushAllQueuedEvents();
    std::cout << "\n" ;

    std::cout << "BEGIN Orbit #3" << std::endl;
    vecShips[2]->trigger( MutinyEvent(false, 15) );
    vecShips[2]->trigger( MutinyEvent(true, 20) );
    vecShips[2]->trigger( LandingEvent() );
    vecShips[3]->trigger( CrashEvent("Fizzle.") );
    vecShips[3]->trigger( MutinyEvent(true, 0) ); //< Should not cause output, since this ship has already crashed!
    std::cout << "END Orbit #3\n" << std::endl;

    pEventManager->FlushAllQueuedEvents();
    std::cout << "\n" ;

    return 0;
}

Comments

1

Qt's event model is instructive

1 Comment

Unfortunately, Qt uses a central event handler in QObject that is simply a big switch statement. It works because Qt apps tend to use Qt's predefined events, which QObject provides overridable handlers for. But in my case, the majority of events are essentially unique to each class -- eg., Spaceship has "crash", "rocket_fired", "powered_up", "health_changed" etc. There is only a handful of events (like "created", "hit" or "destroyed") that are common to all objects.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.