Implementing a simple event system in C++11

Tags: howtos, programming

Published on
« Previous post: Of type lists and type switches — Next post: Simple fractals with Qt 5 shaders »

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.