Implementing a simple event system in C++11
Tags: howtos, programming
I recently started thinking about event handling systems. I have been using Boost.Signals
for some
time now and was pretty happy with it. Then the number of signals spiralled out of control and
forced me to re-think the design. The venerable design patterns book offers the Observer pattern or its modified form, the Publish–subscribe pattern. Both design
patterns involve passing information between possibly unrelated objects.
My goal was to come up with a system that couples objects very loosely. Ideally, the central instance, which I am going to call dispatcher because it makes Tamiko happy, should know barely anything about an event it delivers. Using C++11, I came up with a rather simple and straightforward design.
The following discussion will leave out unsubscriptions by observers because it makes the code samples harder to follow. The code on GitHub, though, has this functionality.
A base event class
Regardless of how anything else is implemented, we need a base class for describing an event. Mine looks like this:
class Event
{
public:
virtual~ Event();
using DescriptorType const char*;
virtual DescriptorType type() const = 0;
};
Not much to see here. The destructor is kept virtual because Event
is a polymorphic base class.
Every derived event needs to implement the type()
function to describe itself. The
DescriptorType
refers to const char*
, meaning that each event uses a const char*
to create a
unique identifier. In practice, this is what it might look like:
class DemoEvent : public Event
{
public:
DemoEvent();
virtual ~DemoEvent();
static constexpr DescriptorType descriptor = "DemoEvent";
virtual DescriptorType type() const
{
return descriptor;
}
};
Note that the DemoEvent
class also offers a static constexpr
descriptor. This permits us to
write DemoEvent::descriptor
when referring to the descriptor of this class. In other words, we do
not need to instantiate anything here.
The dispatcher
The role of the dispatcher is to manage multiple observers that are interested in a certain event.
To this end, we need a predefined interface every observer needs to implement. C++11 offers
std::function
for this purpose. We also need to keep track of which observers are interested in
which event. This is where the DescriptorType
of events comes in. Since DescriptorType
is a
const char*
, we can use it as key in a map! Whenever an event occurs, we then look up its
corresponding type, use this to access the map, and call all observers.
This is the basic interface of the dispatcher:
class Dispatcher
{
public:
using SlotType = std::function< void( const Event& ) >;
void subscribe( const Event::DescriptorType& descriptor, SlotType&& slot );
void post( const Event& event ) const;
private:
std::map< Event::DescriptorType, std::vector<SlotType> > _observers;
};
Using post()
any client can send events to all observers that are interested in them. The
dispatcher keeps track of slots to call using the map. The implementation turns out to be
surprisingly simple:
void Dispatcher::subscribe( const Event::DescriptorType& descriptor, SlotType&& slot )
{
_observers[descriptor].push_back( slot );
}
void Dispatcher::post( const Event& event ) const
{
auto type = event.type();
// Ignore events for which we do not have an observer (yet).
if( _observers.find( type ) == _observers.end() )
return;
auto&& observers = _observers.at( type );
for( auto&& observer : observers )
observer( event );
}
Defining observers
So, how do we use the dispatcher then? By invoking the magic of std::bind
. Let us first define a
simple observer class. In this class, we can also check the underlying event type, for example if we
want to react differently to certain events:
class ClassObserver
{
public:
void handle( const Event& e )
{
if( e.type() == DemoEvent::descriptor )
{
// This demonstrates how to obtain the underlying event type in case a
// slot is set up to handle multiple events of different types.
const DemoEvent& demoEvent = static_cast<const DemoEvent&>( e );
std::cout << __PRETTY_FUNCTION__ << ": " << demoEvent.type() << std::endl;
}
}
};
Again, we can see that the static constexpr
comes in handy. We do not need an instance of
DemoEvent
here. Now, assuming we have a dispatcher variable called dispatcher
, which is very
creative, we can finally use std::bind()
:
int main( int, char** )
{
using namespace std::placeholders;
ClassObserver classObserver;
Dispatcher dispatcher;
dispatcher.subscribe( DemoEvent::descriptor,
std::bind( &ClassObserver::handle, classObserver, _1 ) );
dispatcher.post( DemoEvent() );
}
This also demonstrates the basics of posting an event. It is probably not apparent in this example, but the dispatcher does not need to know anything about events excepts that they are inheriting from the same base class. We can pass arbitrarily complex objects to and fro without changing the coupling between the dispatcher and the observers.
Limitations
A severe limitation of the design in this form is the inability to handle events in different
threads. The post()
interface assumes that it gets passed a const reference
to an event. This
makes sense insofar as we do not know how "heavy" an event is—or whether it makes
sense to be copied. Thus, in case one observer chooses to handle an event in a separate thread, the
event could potentially be destroyed before the observer finished handling it. One solution for this
problem would be to use std::shared_ptr
to pass events around. This at least would solve the
destruction issue—but I am not very firm concerning the thread safety of std::shared_ptr
.
Another issue is a systematic problem: I cannot guarantee the uniqueness of the self-described
events. If a client chooses to implement events A
and B
and both return, say "Foo" as
their event type, the dispatcher will not be able to separate between these events. Currently, I
do not have a solution for this—hence, comments would be highly appreciated!
Update (2021-01-31): An issue in the GitHub repository belonging to this post discusses a potential extension to handle different event types by means of class templates in the dispatcher class. This is a very neat suggestion—check it out.
The code
A slightly more involved implementation is available as the "Events" repository in my GitHub account. The code is licensed under an MIT license.
I hope reading this was both insight- and eventful for you.