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.)
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.
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?
In computer programming, event-driven programming is a programming paradigm in which the flow of the program is determined by events such as user actions (mouse clicks, key presses), sensor outputs, or message passing from other programs or threads.
About Event HandlerEvents are declared using delegates. Events are the higher level of Encapsulation over delegate. A delegate that exists in support of an event is called as an event handler. Event handlers are methods in an object that are executed in response to some events occurring in the application.
An event handler, in C#, is a method that contains the code that gets executed in response to a specific event that occurs in an application. Event handlers are used in graphical user interface (GUI) applications to handle events such as button clicks and menu selections, raised by controls in the user interface.
An event-driven application is designed to detect events as they occur, and then deal with them using some event-handling procedure. Examples of events include: An HTML message has been received (web server) A key has been pressed (text editor)
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:
boost
can be replaced with C++11 features or the std
library. (Only signals2
must remain.)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;
}
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 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With