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:
1 2 3 4 5 6 7 8 9 10 11 |
class MyClass { public: void Foo(int); }; Delegate<void(int)> delegate; MyClass mc; delegate.Bind(mc, &MyClass::Foo); |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
#ifndef DELEGATE_H #define DELEGATE_H /**** delegate primary class template (not defined) ****/ template <typename Signature> class Delegate; /**** delegate partial class template 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) = delete; Delegate(Delegate &&other) : mCallableWrapper(other.mCallableWrapper) { other.mCallableWrapper = nullptr; } ~Delegate() { delete mCallableWrapper; mCallableWrapper = nullptr; } Delegate &operator=(Delegate const &other) = delete; 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> 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, 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
#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 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; PtrToConstMemFun mPtrToConstMemFun; }; /***** 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(); } 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 T *mFunObject; bool mAllocated; }; #endif // CALLABLE_WRAPPER_H |
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:
1 2 |
delegate(10); // overloaded function call operator delegate.Invoke(10); // same effect |
A Signal object contains a vector of delegates, and can bind to many callables that shares the same signature:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
/**************** 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> 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; }; |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#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).