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 logic, 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 all of the problems above but unfortunately the C++ language doesn’t come with a delegate system included; there are a few implementations on the web, which differ in performance and complexity of implementation, 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 memory allocation and runtime dispatch (aka polymorphism), and another faster, though slightly more advanced implementation (I provide a C++17 compliant variant of this version too, which makes for a slightly better interface). In this post I focus on the simpler, runtime-dispatch implementation.
By the way, 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 (funcion object types and pointer to member functions) and a couple of member functions to invoke 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 we need to call on that object:
class MyClass
{
public:
void Foo(int);
};
Delegate<void(int)> delegate;
MyClass mc;
delegate.Bind(mc, &MyClass::Foo);
Internally the delegate chooses the right Bind() overload based on the type of the callable passed as argument; it also supports function object types like (pointers to) free-functions, functors and lambdas:
#ifndef DELEGATE_H
#define DELEGATE_H
/**** delegate primary class template (not defined) ****/
template <typename Signature>
class Delegate;
/**** delegate partial class template specialization for function types ****/
template <typename Ret, typename... Args>
class Delegate<Ret(Args...)>
{
friend class Signal<Ret(Args...)>;
public:
Delegate() : mCallableWrapper(nullptr) {}
Delegate(const Delegate &other) : mCallableWrapper(other.mCallableWrapper->Clone()
{
}
Delegate(Delegate &&other) : mCallableWrapper(other.mCallableWrapper)
{
other.mCallableWrapper = nullptr;
}
~Delegate()
{
delete mCallableWrapper;
mCallableWrapper = nullptr;
}
Delegate &operator=(Delegate const &other)
{
Delegate temp(other);
Swap(temp);
return *this;
}
Delegate &operator=(Delegate &&other)
{
Delegate temp(std::move(other));
Swap(temp);
return *this;
}
//template <typename T>
//void Bind(T &instance, Ret (T::*ptrToMemFun)(Args...))
//{
//mCallableWrapper = new MemFunCallableWrapper<T,Ret(Args...)>(instance, ptrToMemFun);
//}
//template <typename T>
//void Bind(T &instance, Ret (T::*ptrToConstMemFun)(Args...) const)
//{
//mCallableWrapper = new ConstMemFunCallableWrapper<T,Ret(Args...)>(instance, ptrToConstMemFun);
//}
template <typename T, typename PtrToMemFun, typename = typename std::enable_if<std::is_member_function<PtrToMemFunction>::value && std::is_invokable_ret<Ret, PtrToMemFun, Args...>::value>::type>
void Bind(T &instance, PtrToMemFun ptrToMemFun)
{
mCallableWrapper = new MemFunCallableWrapper<T, Ret(Args), PtrToMemFun>(instance, ptrToMemFun);
}
template <typename T>
void Bind(T &&funObj)
{
mCallableWrapper = new FunObjCallableWrapper<std::remove_reference_t<T>,Ret(Args...)>(std::forward<T>(funObj));
}
void Swap(Delegate &other)
{
CallableWrapper<Ret(Args...)> *temp = mCallableWrapper;
mCallableWrapper = other.mCallableWrapper;
other.mCallableWrapper = temp;
}
explicit operator bool() const
{ return mCallableWrapper != nullptr
}
Ret operator()(Args... args)
{
return mCallableWrapper->Invoke(std::forward<Args>(args)...);
}
Ret Invoke(Args... args)
{
return mCallableWrapper->Invoke(std::forward<Args>(args)...);
}
private:
CallableWrapper<Ret(Args...)> *mCallableWrapper;
};
#endif // DELEGATE_H
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, it is the dynamic dispatch mechanism that chooses the right implementation to call when the delegate is invoked.
The delegate 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 represents a specific type of callable:
#ifndef CALLABLE_WRAPPER_H
#define CALLABLE_WRAPPER_H
/***** base callable wrapper class *****/
template <typename Signature>
class CallableWrapper;
template <typename Ret, typename... Args>
class CallableWrapper<Ret(Args...)>
{
public:
virtual ~CallableWrapper() = default;
virtual CallableWrapper *Clone() = 0;
virtual Ret Invoke(Args... args) = 0;
protected:
CallableWrapper() = default;
};
/***** wrapper around a non-const member function *****/
//template <typename T, typename Signature>
//class MemFunCallableWrapper;
//template <typename T,typename Ret, typename... Args>
//class MemFunCallableWrapper<T, Ret(Args...)> : public CallableWrapper<Ret(Args...)>
//{
//private:
//using PtrToMemFun = Ret (T::*)(Args...);
//public:
//MemFunCallableWrapper(T &instance, PtrToMemFun ptrToMemFun) : mInstance(instance), mPtrToMemFun(ptrToMemFun) {}
//Ret Invoke(Args... args) override { return (mInstance.*mPtrToMemFun)(std::forward<Args>(args)...); }
//private:
//T &mInstance;
//PtrToMemFun mPtrToMemFun;
//};
/***** wrapper around a const member function *****/
//template <typename T, typename Signature>
//class ConstMemFunCallableWrapper;
//template <typename T,typename Ret, typename... Args>
//class ConstMemFunCallableWrapper<T, Ret(Args...)> : public CallableWrapper<Ret(Args...)>
//{
//private:
//using PtrToConstMemFun = Ret (T::*)(Args...) const;
//public:
//ConstMemFunCallableWrapper(T &instance, PtrToConstMemFun ptrToConstMemFun) : mInstance(instance), mPtrToConstMemFun(ptrToConstMemFun) {}
//Ret Invoke(Args... args) override { return (mInstance.*mPtrToConstMemFun)(std::forward<Args>(args)...); }
//private:
//T &mInstance;
//PtrToMemFun mPtrToConstMemFun;
};
/**** wrapper around a (const) member function ****/
template <typename T, typename Signature, typename PtrToMemFun>
class MemFunCallableWrapper;
template <typename T, typename Ret, typaname Args..., typename PtrToMemFun>
class MemFunCallableWrapper<T, Ret(Args...), PtrToMemFuncion>
{
public:
MemFunCallableWrapper(T &instance, PtrToConstMemFun ptrToConstMemFun) : mInstance(instance), mPtrToConstMemFun(ptrToConstMemFun) {}
MemFunCallableWrapper *Clone() override
{
return new MemFunCallableWrapper(mInstance, mCallableWrapper);
}
Ret Invoke(Args... args) override { return (mInstance.*mPtrToConstMemFun)(std::forward<Args>(args)...); }
private:
T &mInstance;
PtrToMemFun mPtrToMemFun;
};
/***** wrapper around a function object/lambda *****/
template <typename T, typename Signature>
class FunObjCallableWrapper;
template <typename T, typename Ret, typename... Args>
class FunObjCallableWrapper<T,Ret(Args...)> : public CallableWrapper<Ret(Args...)>
{
public:
FunObjCallableWrapper(T &funObject) : mFunObject(&funObject), mAllocated(false) {} // lvalue (take address)
FunObjCallableWrapper(T &&funObject) : mFunObject(new T(std::move(funObject))), mAllocated(true) {} // rvalue (make copy on the heap)
~FunObjCallableWrapper() { Destroy(); }
FunObjCallableWrapper *Clone() override
{
if (!allocated)
{
return new FunObjectCallableWrapper()
}
}
Ret Invoke(Args... args) { return (*mFunObject)(std::forward<Args>(args)...); }
private:
template <typename U = T, typename = std::enable_if_t<std::is_function<U>::value>> // dummy type param defaulted to T (SFINAE)
void Destroy() {}
template <typename = T, typename U = T, typename = std::enable_if_t<!std::is_function<U>::value>> // dummy type param defaulted to T (SFINAE) + extra type param (for overloading)
void Destroy() { if (mAllocated) delete mFunObject; } // SFINAE-out if T has function type
private:
T *mFunObject;
bool mAllocated;
};
#endif // CALLABLE_WRAPPER_H
Using a template type parameter for the pointer to member function in the Bind function allows to bind a member function whose signature doesn’t match exactly with the one of the delegate’s: what’s needed is that the parameters of the delegates must be convertible to those of the bound function, and the return type of the function must be convertible to the return type of the delegate.
I also found it useful to add support for r-values like temporary lambdas, but since the delegate use reference semantics to store the callables it can’t store the callable as a (non-const) l-value 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 using the normal funcuon call syntax for function object type:
delegate(10); // overloaded function call operator
delegate.Invoke(10); // same effect
A Signal is a container of delegates, it contains a vector of delegate objects, and can bind to many callables as long as they share the same common signature:
#ifndef SIGNAL_H
#define SIGNAL_H
/**************** signal ****************/
#include <vector>
/**** signal primary class template (not defined) ****/
template <typename Signature>
class Signal;
/**** signal partial class template for function types ****/
template <typename Ret, typename... Args>
class Signal<Ret(Args...)>
{
friend class Connection;
public:
//template <typename T>
//Connection Bind(T &instance, Ret (T::*ptrToMemFun)(Args...))
//{
//Delegate<Ret(Args...)> delegate;
//mDelegates.push_back(std::move(delegate));
//mDelegates.back().Bind(instance, ptrToMemFun);
//return Connection(this, mDelegates.back().mCallableWrapper);
//}
//template <typename T>
//Connection Bind(T &instance, Ret (T::*ptrToConstMemFun)(Args...) const)
//{
//Delegate<Ret(Args...)> delegate;
//mDelegates.push_back(std::move(delegate));
//mDelegates.back().Bind(instance, ptrToConstMemFun);
//return Connection(this, mDelegates.back().mCallableWrapper);
}
template <typename T, typename PtrToMemFun>
Connection Bind(T &instance, PtrToMemFun ptrToMemFun)
{
mDelegates.push_back(Delegate<Ret(Args...)>)
mDelegates.back().bind(instance, ptrToMemFun);
return Connection(this, mDelegates.back().mCallableWrapper)
}
template <typename T>
Connection Bind(T &&funObj)
{
Delegate<Ret(Args...)> delegate;
mDelegates.push_back(std::move(delegate));
mDelegates.back().Bind(std::forward<T>(funObj));
return Connection(this, mDelegates.back().mCallableWrapper);
}
explicit operator bool() const
{
return !mDelegates.empty();
}
void operator()(Args... args)
{
for (auto &delegate : mDelegates)
delegate(std::forward<Args>(args)...);
}
void Invoke(Args... args)
{
for (auto &delegate : mDelegates)
delegate.Invoke(std::forward<Args>(args)...);
}
private:
void Unbind(CallableWrapper<Ret(Args...)> *callableWrapper)
{
for (auto it = mDelegates.begin(), end = mDelegates.end(); it != end; ++it)
if (it->mCallableWrapper == callableWrapper)
{
mDelegates.erase(it);
return;
}
}
std::vector<Delegate<Ret(Args...)>> mDelegates;
};
#endif // SIGNAL_H
Signal::Bind() returns a Connection object: Connection objects are what allows an observer to tell the signal that it doesn’t need to be notified anymore (as in the case when it has been destroyed), so the signal can delete the delegate and avoid undefined behavior when trying to notify a destroyed callable object:
#ifndef CONNECTION_H
#define CONNECTION_H
template <typename Signature>
class Signal;
class Connection
{
public:
template <typename Ret, typename... Args>
Connection(Signal<Ret(Args...)> *signal, CallableWrapper<Ret(Args...)> *callableWrapper) : mSignal(signal), mCallableWrapper(callableWrapper), mDisconnectFunction(&DisconnectFunction<Ret, Args...>) {}
void Disconnect() { mDisconnectFunction(mSignal, mCallableWrapper); }
private:
void (*mDisconnectFunction)(void*, void*);
void *mSignal;
void *mCallableWrapper;
template <typename Ret, typename... Args>
static void DisconnectFunction(void *signal, void *callableWrapper)
{
static_cast<Signal<Ret(Args...)>*>(signal)->Unbind(static_cast<CallableWrapper<Ret(Args...)>*>(callableWrapper));
}
};
#endif // CONNECTION_H
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:
#include "delegate.hpp"
#include <iostream>
#include <vector>
SIGNAL_RET_ONE_PARAM(MySig, int, double);
MySig sig;
int FreeFunction(int, int)
{
std::cout << "in free function" << std::endl;
return 1;
}
class MyClass
{
public:
MyClass(int i) : i(i)
{
mConnections.push_back(sig.Bind(*this, &MyClass::MemberFunction));
mConnections.push_back(sig.Bind(*this, &MyClass::ConstMemberFunction));
mConnections.push_back(sig.Bind(&MyClass::StaticMemberFunction));
mConnections.push_back(sig.Bind(*this));
mConnections.push_back(sig.Bind(*static_cast<MyClass const*>(this)));
}
~MyClass() { for (auto &connection : mConnections) connection.Disconnect(); }
int MemberFunction(double d) { std::cout << "in member function" << std::endl; return int(++i * d); }
int ConstMemberFunction(double d) const { std::cout << "in const member function" << std::endl; return int(i * d); }
int operator()(double d) { std::cout << "in overloaded function call operator" << std::endl; return (int)(++i + d); }
int operator()(double d) const { std::cout << "in const overloaded function call operator" << std::endl; return (int)(i + d); }
static int StaticMemberFunction(double d) { std::cout << "in static member function" << std::endl; return int(10 + d); }
private:
int i;
std::vector<Connection> mConnections;
};
int main(int argc, char *argv[])
{
{
MyClass mc(10);
sig(1.2);
}
std::cout << "**********************" << std::endl;
sig(1.2); // empty signal
std::cout << "**********************" << std::endl;
int i = 10;
sig.Bind([i](double d) mutable -> int { std::cout << "in temp lambda" << std::endl; return ++i; });
auto lambda = [&i](double) { std::cout << "in lambda" << std::endl; return ++i; };
sig.Bind(lambda);
sig(1.20);
return 0;
}
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).