code-section

[C++] A Simple Event System

Dec 24, 2013

In this post I describe a simple event system in C++. The main purpose of an event system is to have some code in a part of the application (module) be executed whenever an event in another module takes place. An event system is a great tool for increasing code modularity and reducing dependencies and unnecessarily complex class hierarchies. Our approach is simple but functional - it's basically a list of function/method pointers.

The event system uses Don Clugston's Fastest Possible C++ Delegates. Clugston's delegates are nice since they work on a wide range of compilers, fast, their syntax is nice, and allow you to use free functions as well as member functions. We could use boost::function and boost::bind instead, but using them incurs an additional dynamic memory allocation per delegate object instance that we could do without, as well as introducing a dependency on boost, which is a HUGE library.

Events and Delegates

There are two types of objects in the event system: an event, and a delegate (or callback). An event is basically a list of delegates. When an event is triggered, the associated delegates are invoked sequentially. A delegate is either a free function or a member function on an object.

The type of an event determines the type of delegates that can be added to that event. The type of an event is determined by the return type as well as the parameter types of the delegates. The DECLARE_EVENT macro makes this easy.

DECLARE_EVENT( retType, arg1Type, arg2Type, arg3Type... ) eventVarName;

A module can then expose an event object so that event handlers can be attached to it. The TRIGGER_EVENT macro makes triggering an event (i.e calling the attached callbacks) easy.

Example Code: Basic Usage

class Window
{
    public:
    DECLARE_EVENT( void, int mouseButton, int x, int y ) eventMouseButtonPressed;
    DECLARE_EVENT( void, int mouseButton, int x, int y ) eventMouseButtonReleased;
    DECLARE_EVENT( void, int mouseButton, int x, int y ) eventMouseMoved;

    int ProcessOperatingSystemMessages( MSG msg )
    {
        if( msg.code == WL_LBUTTONDOWN )
            TRIGGER_EVENT( eventMouseButtonPressed( MB_LEFT, msg.x, msg.y ) );
        // ...
    }
};


// Meanwhile in main.cpp
void OnMouseButtonPressed( int mouseButton, int x, int y )
{
    // ...
}

void OnMouseButtonReleased( int mouseButton, int x, int y )
{
    // ...
}

int main()
{
    Window wnd;
    wnd.eventMouseButtonPressed.InsertDelegate( OnMouseButtonPressed );
    wnd.eventMouseButtonReleased.InsertDelegate( OnMouseButtonReleased );
    // ...
}

The Window class can have multiple implementations to present a platform-independent window interface for an application for example. The ProcessOperatingSystemMessages() method would be different for each OS. The TRIGGER_EVENT macro will call each delegate in the event, but it will neglect any return values. If some logic needs to be applied based on the return values from delegates, then the code to invoke the delegates for the event will look different.

Example Code: Checking Return Values

for( HDELEGATE hDG = eventMouseButtonPressed.GetFirstDelegate();
    hDG != eventMouseButtonPressed.GetEndDelegate();
    hDG = eventMouseButtonPressed.GetNextDelegate( hDG ))
{
    // A callback returns 'true' if it has handled the event.
    bool ret = eventMouseButtonPressed.GetDelegate( hDG )( MB_LEFT, mouse.x, mouse.y );
    if( ret )
        break;
}

The InsertDelegate() method on the event class returns a handle (HDELEGATE) to the added delegate. This handle can then be used to remove the delegate in constant time. Otherwise, a linear search will have to be made to remove a specific delegate from an event. In most cases, event handlers are set at the beginning of program execution and they will not have to be removed until the program exits.

Example Code: Removing Callbacks

HDELEGATE hDlg = wnd.eventMouseButtonPressed.InsertDelegate( OnMouseButtonPressed );
wnd.eventMouseButtonPressed.RemoveDelegate( hDlg );

The examples above have used free functions for the delegates. In order to add a member function as a delegate, the MakeDelegate() function is needed.

Example Code: Member Functions and +=

bool OnMouseButtonPressed( int button, int x, int y ); class UserInputProcessor { public: bool OnMouseButtonPressed( int button, int x, int y ) { /** do stuff ** } }; int main() { Window wnd; UserInputProcessor uip; // Add the free function. Using the += operator. wnd.eventMouseButtonPressed += OnMouseButtonPressed; // Add the member function. wnd.eventMouseButtonPressed += MakeDelegate( &uip, &UserInputProcessor::OnMouseButtonPressed ); }

Obviously you have to ensure that the object whose method will be called when the event is triggered will remain alive as long as the event is alive and can be triggered. The system does not use any sort of reference counting or smart pointers, so object lifetime management is not automatic.

Final Thoughts

Well that's about it for the usage of this simple event system. There are features in an event system that one might need or find useful which are missing from this system, like asynchronous or delayed delegate invocation, which are not implemented here.

Newer features of C++ (i.e. lambdas) can also be utilized to make the system easier to use.

I hope this was helpful to someone, and I hope to refine it further. If you have any thoughts, corrections, questions, or suggestions, feel free to share them in the comments below.

Source Code

The event system is contained in a single .h file, but you will also need Don Clugston's code to use it.