C++ Delegates
I’ve always really liked working with delegates in Unreal Engine 4. It’s so easy to use, works in various different ways and after a while of using them, I wanted to try to implement them myself.
As you might know, it’s not straight forward at all to store and execute a method in a generic way in C++. A function pointer might be a solution but it’s not flexible as it’s very type-restricted and can’t have any extra data a lambda would have. The standard template library solved this by providing std::function<>
which is sort of a more flexible and fancy function pointer. However, I’ve found that both function pointers and std::function are such a headache to work with!.
Take a look at an extremely simple example of executing a member function with a int(float)
signature. Straight forward right? Well, not really…
Foo foo;
// Function pointer. Arrrghh
int(Foo::* pFunctionA)(float) = &Foo::Bar;
(foo.*pFunctionA)(42);
// std::function. Object itself passed as variable (makes kind of sense but extremely awkward!)
std::function<int(Foo&, float)> pFunctionB = &Foo::Bar;
pFunctionB(foo, 42);
// Delegate. Elegant and straight forward
auto pFunctionC = Delegate<int, float>::CreateRaw(&foo, &Foo::Bar);
pFunctionC.Execute(42);
Using a custom solution looks far more elegant. The syntax for function pointers are such a nightmare even for the most simple cases.
Besides usability, having a custom solution allows for some neat optimizations and control which I will discuss below.
If you’ve been working with for example C#, you might be familiar with delegates and events already. Unfortunately, C++ has no functionality as powerful as this. What delegates allow you to do, is store a function in an object to execute at a later time. Pretty much what we’re trying to achieve here.
The full source of this can be found on GitHub. I’d recommend taking a look at it while reading this because it will give more context.
Terminology
I’ve following Unreal Engine’s terminology and created the two main Delegate “types”. You could say there are more but they are functionally identical:
Delegate<RetVal, Args>
- Binds a single function. Pretty much like a
delegate
in C#
- Binds a single function. Pretty much like a
MulticastDelegate<Args>
- Binds multiple functions. Similar to an
event
in C#
- Binds multiple functions. Similar to an
Unlike a function pointer, which can only point to the address of a global/static or member function, a delegate allows various types of functions to be bound to it, including lambda’s.
Usage
Delegate
A Delegate allows you to store a function and its data to execute later. Unlike a regular function pointer, this is much more flexible because it supports global/static functions, member function, lambda’s and member functions taking into account the reference count of a shared_ptr.
Below is an example of binding a lambda to a delegate that returns an int
and has a float
as a parameter.
struct Foo
{
int Bar(float a)
{
printf("Raw delegate parameter: %f", a);
return 10;
}
};
Delegate<int, float> del;
Foo foo;
del.BindRaw(foo, &Foo::Bar);
int out = del.Execute(20);
printf("Lambda delegate return value: %d", out);
This outputs the following:
Output:
Lambda delegate parameter: 20
Lambda delegate return value: 10
MulticastDelegate
Building upon the above, we can create a delegate that can hold more than one reference to a function. This is what the MulticastDelegate is for. Multiple callbacks can be bound and when the MulticastDelegate is executed, it will call all the functions bound to it. You can compare this with an event
in C#.
What makes this so powerful, is that multiple “types” of functions can be bound to this.
Below an example. Because multiple callbacks can be bound to a single MulticastDelegate, it doesn’t make sense for such a delegate to have a return type. Because of that, a function bound to a MulticastDelegate must always have a void
return type.
struct Foo
{
void Bar(float a)
{
printf("Raw delegate parameter: %f", a);
}
};
MulticastDelegate<float> del;
del.AddLambda([](float a)
{
printf("Lambda delegate parameter: %f", a);
}, 90);
Foo foo;
del.AddRaw(&foo, &Foo::Bar);
del.Broadcast(20);
Output:
Lambda delegate parameter: 20
Raw delegate parameter: 20
Implementation
Now comes the meaty part! When working on this, I was reading the book Effective Modern C++ by Scott Meyers and there are a few awesome chapters about template programming and type deduction and I highly recommend reading it if some of the code snippets below are unclear. This little project was mainly meant to get familiar with template programming.
A lot of details will be omitted here because otherwise the post size will explode, I will only include the fundamental parts.
Delegate
As you might expect, this solution will need a load of templates because every function signature describes its own type. Looking at the few examples below, we can extract what defines the ‘type’ of a function: the return value and the parameters.
Eg.
int Foo(float a, char b); // == int(*)(float, char)
int Clazz::Foo(const float& a, char b); // == int(*Clazz::)(const float&, char)
[this](float a, char b){}; // == ????
The basics of a delegate are fairly simple; It’s a structure that has the address to a function and potentially holds data to pass through when the function is called. Because We want to support multiple function types, we must define an interface:
template<typename RetVal, typename... Args>
class IDelegate
{
public:
virtual RetVal Execute(Args&&... args) = 0;
};
As mentioned, a delegate is defined by the function ‘signature’, the class is already templated using its return value and parameters.
Now with this base class, we can start defining our different callback types. I’ve created the four that cover pretty much all the bases:
- Static: For static or global functions that don’t have a reference to the object
- Raw: Member functions
- Delegate: Lambda’s. Here we store the capture list
- SharedPtr: A delegate that stores a
std::weak_ptr
of the object. This way, it is aware of object lifetime.
Here, I want to focus on the LambdaDelegate because that one will become the most interesting. The implementation is pretty straight forward and is also quite simple for the other types.
template<typename TLambda, typename RetVal, typename... Args>
class LambdaDelegate : public IDelegate<RetVal, Args...>
{
public:
explicit LambdaDelegate(TLambda&& lambda)
: m_Lambda(std::forward<TLambda>(lambda))
{}
RetVal Execute(Args&&... args) override
{
return (RetVal)((m_Lambda)(std::forward<Args>(args)...);
}
private:
TLambda m_Lambda;
};
Where all the implementations come together is in the final Delegate class. This class has a bunch of functions that allow you to bind all implemented types of delegates. You might notice the dynamic allocation, not great, I’ll address that in the next section.
template<typename RetVal, typename... Args>
class Delegate
{
public:
using IDelegateT = IDelegate<RetVal, Args...>;
template<typename TLambda, typename... Args2>
static Delegate CreateLambda(TLambda&& lambda)
{
Delegate handler;
handler.m_pAllocator = new LambdaDelegate<TLambda, RetVal(Args...)>(std::forward<TLambda>(lambda));
return handler;
}
template<typename LambdaType, typename... Args2>
inline void BindLambda(LambdaType&& lambda, Args2&&... args)
{
Release();
*this = CreateLambda<LambdaType >(std::forward<LambdaType>(lambda));
}
RetVal Execute(Args... args) const
{
assert(m_Allocator.HasAllocation() && "Delegate is not bound");
return GetDelegate()->Execute(std::forward<Args>(args)...);
}
private:
IDelegateT* m_pAllocator;
};
Optimization: The InlineAllocator
As said, the LambdaDelegate
stores the data of all elements in the capture list. That is the nature of a lambda. This means that naturally, a lambda needs to be dynamically allocated because the size of this data is not known beforehand. Besides, having different implementations for each delegate type, forces us to use dynamic allocations. This could have some bad performance implications. However, there’s a neat way of improving this and make sure small lambda’s don’t necessarily need to be dynamically allocated. This is where an InlineAllocator
comes in and it will replace the dynamic allocation.
This allocator will allocate inline until it crosses a specified size (32 bytes in this case). What this does is make sure a Delegate always has a fixed size until it cross the 32 bytes threshold. In most cases this is enough and thus executing the delegate will have a smaller execution overhead, especially when multiple delegates are fired in sequence. Using a union
makes it easy to do this:
union
{
char Buffer[MaxStackSize];
void* pPtr;
};
MulticastDelegate
A MulticastDelegate
builds upon the regular delegate. It is simply a list with a ‘handle’ pointing to a bound delegate. This handle is a unique ID and is there to be able to later unbind the callback from the delegate.
Executing the a MulticastDelegate basically boils down to a for-loop.
An interesting catch is the fact that it’s possible to unbind functions during execution. This can be worked around by clearing callbacks instead of removing them during execution so that they can later be emplaced by new function bindings. The Lock()
makes sure this is handled properly.
void Broadcast(Args ...args)
{
Lock();
for (size_t i = 0; i < m_Events.size(); ++i)
{
if (m_Events[i].first.IsValid())
{
m_Events[i].second.Execute(std::forward<Args>(args)...);
}
}
Unlock();
}
Extra
Unreal Engine has a pretty cool extra feature called ‘payloads’. This gives you the ability to store data in the delegate at bind-time. This is doing using a std::tuple
. I’ve implemented this myself too but I’ve never really found much use in having it so. However, it’s a great exercise in learning template programming as it makes the implementation much more complex in an interesting way. You can find this in the source code on GitHub.
Moving on
This post is already getting a bit long so I’d definitely recommend taking a look at the full source code, it’s mainly all just in a single header
In the end, I think these Delegates are extremely powerful and I use them wherever I can in personal projects. This makes the code well decoupled while still having structure.
Feel free to head over to the repository and copy, modify and/or use the code for yourself!