Sponsored By

C++ Events

In this reprinted <a href="http://altdevblogaday.com/">#altdevblogaday</a> technical piece, GamesLab's Christian Schladetsch introduces a mechanism for implementing a Subscriber/Publisher pattern to your system architecture, by including just one header f

Christian Schladetsch, Blogger

November 2, 2011

8 Min Read

[In this reprinted #altdevblogaday technical piece, GamesLab's Christian Schladetsch introduces a mechanism for implementing a Subscriber/Publisher pattern to your system architecture, by including just one header file.] All interactive game architectures are, by their nature, forced to be somewhat event-driven. There are various hardware-based events for input, audio and network systems, etc. Furthermore, in modern game systems, events are often used to drive many other aspects, such as:

  • handling abstracted input from GUI controls, such as ButtonClicked and MouseEnter

  • producer/consumer models for long-running tasks such as path-finding

  • collision events emitted from the physics simulator

  • processing abstracted network events above the raw hardware layer

So there is a requirement for an efficient, flexible and expressive event system for C++, which is unfortunately lacking from the standard library. Of course, there are mechanisms in the Boost libraries that can be used to implement such event-based systems, but game developers are generally wary of adding a dependency on boost-related code to their source base. Introduction This article will introduce a multi-cast event model that consists of exactly one header file which has exactly two external dependencies. These are the standard C++ and headers. The library resides entirely in the header; there are no associated source files or libraries to link with. Events from this system can delegate to functions, methods, or other events. The delegate methods can be const or non-const, and the arguments can be any mix of value or reference types. Events are copy-constructable and assignable. The maximum number of arguments in an event signature is fixed to eight for this implementation, but that can be increased by changing a constant and rebuilding the header. So here's some example use, given the one header file:

#include "Events/EventP.h"
#include 

using namespace std;
using namespace Schladetsch::Events;

class Foo
{
public:
  void Method(int num, const string &str);
};

void Fun1(int num, const string &str);
void Fun2(int num, const string &str);

int main()
{
  // make an event that has two parameters
  Event<int, const string&> event;

  // add a delegate method
  Foo foo;
  event.Add(foo, &Foo::Method);

  // add a delegate function
  event.Add(Fun1);

  // fire the event: the foo.Method will be called,
  // as well as Fun1
  event(42, string("Hello, Events"));

  // remove Fun1 from the event
  event.Remove(Fun1);
  event(123, string("Fun1 not called"));

  // it is perfectly safe to copy events
  Event<int, const string&> other(event);
  other.Add(Fun1);
  other(456, "Both Foo::Method and Fun1 called");

  // we can also 'chain' events: by adding one event to another,
  // the added event will be fired when the parent event is fired
  Event<int, const string&> chained;
  chained.Add(Fun2);
  event.Add(chained);
  event(789, "Foo::Method called, as well as firing chained, which will call Fun2");

  return 0;
}

Events are templates that build the signature of supported delegates from their template type parameters. The interface to the system is minimal, with just two methods 'Add' and 'Remove', to add and remove delegates from events. Invoking, or firing, the event looks just like a function call. When the event is fired, all delegates that are stored in the event are invoked in order that they were added. I could have added operator overloading for += and -= to add and remove delegates, as used in C#, but I considered that a little too twee. Architecture The system is based on the idea of decoupling the actual delegate from the way that it is invoked. The event object itself stores a list of pointers to generalized delegates. When the event is fired, it iterates through its delegates, passing the arguments to each. As such, please use reference types for large objects, including strings – but you do that anyway. Along with the base Invoker arity-type (see below) stored in event instances, there are arity-types for delegated functions, const methods, non-const methods, and events. 'Delegated events' in this sense are used for chaining events together, such that when one event is fired, the next chained event is also fired. Syntactically, chaining events is exactly like adding a new delegate to an event. I used the term 'arity-type' above to describe a collection of C++ types that vary only by the arity (number of meaningful type parameters) that they support, but are otherwise semantically equivalent. The general pattern used to implement an arity-type is:

// forward declare the general case
template<int Arity>
struct ArityType;

// specialise for the case of no arguments
template <>
struct ArityType<0>
{
  template <class T0, class T1, ..., class Tn>
  struct Given
  {
     // implementation for arity-0
  };
};

// ...

// specialize for the case of m arguments
template <>
struct ArityType<m>
{
  template <class T0, class T1, ..., class Tn>
  struct Given
  {
     // implementation for arity-m, using type arguments T0...Tm
  };
};

If you think this gets tedious for arities up to eight arguments, with four different delegate types (function, method, const method, chained event), plus the base invoker type – you're absolutely correct. That's why I pulled out the big guns to help with the implementation. Implementation The system was made using the Boost.Prepocessor library, using both local and file vertical iteration. This provided a way to deal with multiple arities throughout. The final header file, EventP.h, is created by running the source headers through the C++ pre-processor and manually editing the result. Alternatively, one can use the underlying headers directly by including Event.h instead, but this will require that you have all the library headers, as well as Boost.Preprocessor header files available. A benefit of using the post-processed headers is that compilation time is kept to a minimum as only two external headers are included: the standard used to store delegates within an event, and for tr1::shared_ptr. Installation Add EventP.h to a location in your include path. This is not meant to be a human-readable file, but it is useful when debugging, as it contains the post-processed output from the source, which is available as a download from my Google Code projects page. The default fully-qualified type name for an event is Schladetsch::Events::Event. You probably don't want to use that name, so set the SCHLADETSCH_NAMESPACE pre-processor symbol to something else before including EventP.h. Improvements There is a single virtual method call required to invoke each delegate within an event when the event is fired. This implementation is not thread-safe. This post has described what and where, but not much of why or how. More work is required to describe how to use these types of systems well, and why they are useful to modern game development practices. Other Work As pointed out in the comments below, there are other similar libraries available, such as those supplied by Stefan Reinalter and Don Clugston. Conclusion This post introduced a mechanism that you can use to implement a Subscriber/Publisher pattern (also known as Signals/Slots) to your system architecture, by including just one header file. Thanks to Chris Regnier for the excellent comments. I hope you find this system useful, and I appreciate all feedback. [This piece was reprinted from #AltDevBlogADay, a shared blog initiative started by @mike_acton devoted to giving game developers of all disciplines a place to motivate each other to write regularly about their personal game development passions.]

Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like