C++ delegates

In a previous article I wrote about the ubiquitous observer pattern, which is all about objects (called observers or listeners) that need to be notified when other objects in the application change or update their state (called subjects).  While the observer pattern nicely decouples subjects and observers, allowing for cleaner and more reusable code, I think that using abstract interfaces with inheritance is too intrusive and somewhat clunky, and reintroduce that coupling that we’re striving to avoid (or at least reduce) in our code.

In my game engine I wanted to be able to let some parts of code, be it a game subsystem like the input system or an object like a GUI widget, communicate with other objects, a player-controlled entity or a GUI button listener for example, in a totally decoupled, type-safe and efficient manner. Such a mechanism is central to an application like a game, where object “talks” to each other continuously, and is at the base of an event system. In addition, I’d like to let my subjects know when an observer is destroyed, so I don’t get undefined behavior when trying to notify an extinct observer.

Delegates are a smart solution to the problem above. There are a few implementations on the web, which differ in performance and simplicity, but all use some kind of type erasure and template magic. I chose two of them, one simpler but less efficient since it uses dynamic allocation and dynamic dispatch, and another faster, though slightly more advanced implementation (I provide a C++17 compliant variant of this version too).

All the code that follows can be found on my GitHub repo.

C++ delegate implementation: using polymorphism and dynamic dispatch

A delegate’s interface is simple: it exposes Bind() member functions templates for binding callables and a couple of member functions to call the delegate (an overloaded function call operator and an Invoke() method that performs the same operation).

Binding a callable is easy, just call the Bind() member function with the object instance and the pointer to the member function:

Internally the delegate choose the right Bind() overload based on the type of the callable passed as argument; it supports function object types (functors) and lambdas:

The kind of type erasure used here is the one provided by inheritance and polymorphism: the delegate doesn’t know the actual (runtime) type of the callable, is the dynamic dispatch mechanism that chooses the right implementation to call when the delegate is called.

I choose to make Delegate move-only, since it manages an heap-allocated object that represents the callable to be invoked: each Bind() overload dynamically creates an object of type derived from CallableWrapper: each derived class is a wrapper around a specific type of callable:

I found it useful to add support for rvalue functors like lambdas, but since the delegate use reference semantics to store the callables it can’t store the callable as a reference in that case, so it allocates a copy of the callable on the heap and stores a pointer to it. Naturally, it must remember to delete the pointer and free the associated memory on destruction (I used a little SFINAE here to support pointers to free functions without compiler errors).

The delegate can be invoked using the Invoke() method, or just by calling the delegate:

A Signal object contains a vector of delegates, and can bind to many callables that shares the same signature:

Signal::Bind() returns a Connection object: Connection objects are what allows an observer to tell the delegate that it has been destroyed, so the delegate can Unbind() the callable and avoid undefined behavior when trying to call a destroyed callable object:

An object can store these connection objects when it binds to the delegate, and use the Disconnect() method of the Connection to disconnect itself from the delegate before destruction. The Connection class has no template parameters, and uses another type of type erasure to store the associated delegate and callable: the signal and the callable wrapper are stored as void pointers and a function saves the signature of both and then recasts the pointers to the correct types.

This is an example application that shows how a delegate can store even temporary functors (function objects/closure objects from lambdas) and how an object can disconnect itself from a Signal:

I find this solution very elegant and simple, with no intrusive inheritance and interfaces, and I use this implementation effectively everywhere in my game engine. It is not so efficient, though: having to allocate dynamically callable wrapper objects is not great, and the need for dynamic dispatch means  that performance is going to take a hit, so in a future post I’m going to describe another implementation that is more efficient (and I think even more clever).

 

Leave a Reply

Your email address will not be published. Required fields are marked *