«

Generic C++ delegates

I like modular programming - the style where each separate part of the functionality knows nothing about any other part. But the modules need to communicate with each other somehow. We can pass callbacks to each other but in that case we can create dependencies - for example, a pointer to member function in C++ looks like this:

int(SomeClass::*ptr)(float);

Here ptr is an actual pointer and SomeClass is the name of the class - owner of this function. As you can imagine, if some class needs to accept a function pointer to another class, then using raw function pointers creates a coupling between them. We can use global or static functions - in that case we don’t need to specify a class name (because there’s no one). But using this types of functions will complicate the code.

The problem can be solved if instead of passing function pointers we’ll pass some generic wrapper - a delegate. Later this wrapper can be called and a call will be delegated to the actual function which delegate wraps. Sounds easy, right? And in fact it’s easy and there many many ready solutions. My favorites are The Impossibly Fast Delegates, Generic type-safe delegates and Delegates On Steroids. And basically my implementation is a mix of aforementioned code with small additions. So, let’s start.

First, let’s found how can we pass a function and call it later. One way is to pass a function pointer as a function argument, store this pointer and call it later. In following example I’m not storing it but call immediately:

int foo(int(*funcPtr)(int))
{
  return funcPtr(10);
}

int test(int a)
{
  return a;
}

foo(&test);

Another option is to pass function pointer as non-type template parameter. In this case we don’t need to keep a pointer - the whole function was created around specified pointer:

template<int(*FuncPtr)(int)>
int foo()
{
  return FuncPtr(10);
}

int test(int a)
{
  return a;
}

foo<&test>();

But which us better? What to use? Of cource there’s no answer. It depends. If you don’t know before which function you need to use as a callback, then passing a function pointer is the only option. But if you know, then passing it as template argument can be a good choice. Check this example:

int call1(int(*funcPtr)(int))
{
  return funcPtr(10);
}

template<int(*F)(int)>
int call2()
{
  return (F)(10);
}

int test(int a)
{
  return a;
}

int main()
{
  int i{0};

  i += test(5);
  i += call1(&test);
  i += call2<&test>();

  return i;
}

I used https://gcc.godbolt.org/ to compile it. Thought it’s hard to get useful output - compilers are damn smart and produce optimized code - with gcc compiler and O1 optimization I got the following assembly output.

test(int):
        mov     eax, edi
        ret
call1(int (*)(int)):
        sub     rsp, 8
        mov     rax, rdi
        mov     edi, 10
        call    rax
        add     rsp, 8
        ret
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:test(int)
        call    call1(int (*)(int))
        add     eax, 15
        add     rsp, 8
        ret
        sub     rsp, 8
        mov     edi, OFFSET FLAT:std::__ioinit
        call    std::ios_base::Init::Init()
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:std::__ioinit
        mov     edi, OFFSET FLAT:std::ios_base::Init::~Init()
        call    __cxa_atexit
        add     rsp, 8
        ret
----

You don’t need to know an assembly to find one interesting thing - the call to call2() was completely optimized out. And the only call here is a call to call1() function which calls the supplied callback. In other words - the template wrapper was replaced by the compiler to the actual callback invokation (here it also optimized out)! It’s hard to tell will the compiler optimize in the same way in real big project but at least this output gives some hope that it will.

Knowing this we can start to implement our delegate. In c++ we have a bunch of callable objects and we can split them in three categories:

  • global functions - functions declared globally and static functions. The pointer to such a function will be passed to our delegate. For example &globalFunction will return the address of a function (& is optional). We can be sure we will not have problems calling it later, because this functions are always exist during program lifetime.

  • member functions - members of a class or a struct. The pointer to this function should be called on valid object and it’s developer’s responsibility to keep an object alive. The member function pointer can be written, for example, int(SomeObj::*funcPtr)(int) and called obj→*funcPtr(42), where obj is a pointer.

  • different callable objects that are not fitting in previous categories. This can be functors, lambdas, std::function objects. Thought lambda can be casted to a function pointer and treated as global/static function such a pointer can’t be used as template argument. Moreover, only lambdas without capture can be casted. This is why I put lambdas in this category.

All that means that we should manage 3 cases in our delegate implementation. So let’s do it.

template<typename T>
class Delegate;

template<typename Ret, typename ...Args>
class Delegate<Ret(Args...)>
{
	using CallbackType = Ret(*)(shared_ptr<void>, Args...);

public:
	Ret operator()(Args... args)
	{
		return callback(callee, args...);
	}

    bool operator==(const Delegate& other)
	{
		return callee == other.callee && callback == other.callback;
	}

private:
	shared_ptr<void> callee{ nullptr };
	CallbackType callback{ nullptr };

private:
	Delegate(shared_ptr<void> obj, CallbackType funcPtr) : callee{ obj }, callback{ funcPtr }
	{
	}
}

Here we created an incomplete base template class and a specialization. This is simply a cosmetic stuff - I like more Delegate<int(int, float)> signature than Delegate<int, int, float>. Next we declared a callback type Ret(*)(shared_ptr<void>, Args…​) - the function that accepts arguments that should be passed to supplied callback and an object - the callee - which we’ll use to call supplied callback on. This callee will be a pointer to a class/struct instance or a pointer to a lambda/functor and nullptr for global/static functions. Let shared_ptr<void> type scare you not - it will be casted to correct type in elegant manner. Also there’re a private constructor - mainly because constructors in c++ can’t be called with explicit template parameters and for ceation we’ll use a factory function, comparison operator - for having only single callback of the same time and for removing of a callback, and a call operator, so our delegate can be called as a functor or even be passed to another delegate!

Now let’s add some meat to our skeleton. The simplest case is a static/global function case:

public:
	template<Ret(*funcPtr)(Args...)>
	static Delegate create()
	{
		return Delegate{ nullptr, &globalCaller<funcPtr> }; // nullptr as first parameter because static/global functions can be called directly
	}

private:
	template<Ret(*funcPtr)(Args...)>
	static Ret globalCaller(shared_ptr<void>, Args... args)
	{
		return funcPtr(args...);
	}

Nothing really complicated here - we just defined a static create function (which calls the private constructor) and a wrapper for the callback. This wrapper is stored for later use. I’ll repeat it here - the standard doesn’t allow to call a constructor with explicit template arguments. That’s why we need create factory function.

The downside of template here is that we need to manually type template argument - it can’t be deduced.

Now we can create and call our first delegate:

int global(int a, float b)
{
	return a + static_cast<int>(b);
}

Delegate<int(int, float)> d{ Delegate<int(int, float)>::create<&global>() };
d(10, 5.0f);

Member function case is slightly more difficult:

public:
	template<typename T, Ret(T::*funcPtr)(Args...)>
	static Delegate create(shared_ptr<T> obj)
	{
		return Delegate{ obj, &memberCaller<T, funcPtr> };
	}

private:
	template<typename T, Ret(T::*funcPtr)(Args...)>
	static Ret memberCaller(shared_ptr<void> callee, Args... args)
	{
		return (static_cast<T*>(callee.get())->*funcPtr)(args...);
	}

Here we have overloaded create() function. And here we need to pass a pointer to existing object which will be stored for later use as well as a wrapper. I decided to use shared_ptr because I want to be sure that object is valid when I call a delegate. With shared_ptr I have this guarantee. The memberCaller() wrapper casts the void* pointer to the provided type so we can say we have some sort of type safety here.

And that’s how it can be created and called:

struct UserStruct
{
	int member(int a, float b)
	{
		return a + static_cast<int>(b);
	}
};

Delegate<int(int, float)> d{ Delegate<int(int, float)>::create<UserStruct, &UserStruct::member>(make_shared<UserStruct>()) };
d(10, 5.0f);

The final case is a functor case. Here we don’t have a function pointer but only the callable object.

public:
	template<typename T>
	static Delegate create(shared_ptr<T> t)
	{
		return Delegate{ t, &functorCaller<T> };
	}

private:
	template<typename T>
	static Ret functorCaller(shared_ptr<void> functor, Args... args)
	{
		return (*static_cast<T*>(functor.get()))(args...);
	}

We have another overloaded create function. We can go wild here and add different compile time checks (for example the check that passed parameter is a callable object) and add readable error message if requirements are violated. But this signature will report about the problems anyway, maybe not in a friendly manner. As in the case with a member function we cast our functor to right type in a functorCaller() wrapper, so no type problems here.

The tricky part is to create a lambda shared pointer. As you may know there’s no strict type for lambda. Instead, on every lambda creation new type will be introduced. And this code typeid([](){}).name() == typeid([](){}).name() will return false. In order to create a necessary shared_ptr I created this function:

template <typename T, typename L = typename std::decay<T>::type>
shared_ptr<L> make_shared_lambda(T&& t)
{
	return make_shared<L>(forward<T>(t));
}

It’s not ideal - the underlying lambda will be copied/moved. Anyway, now we can use functors with a delegate:

auto ptr = make_shared_lambda([](int a, float b)->int
{
	return a + static_cast<int>(b);
});

Delegate<int(int, float)> d{ Delegate<int(int, float)>::create(ptr) };
d(10, 5.0f);
The main point of a delegate - call underlying function later. And we want to be sure that callable object exist. That’s why we need to track the object’s lifetime.

As you probably noticed - the declaration of the delegate is pretty verbose. Can it be simplified? I beleive it can with macros and template magic, but I prefer to have a helper class. This class will handle adding and removing of delegates. I call it - Dispatcher. Imagine some abstract Button class. It can have a Dispatcher for some event - a click, for example. Now every entity that want to listen for this click event can add a delegate to this Dispatcher. And when the real event triggers this Dispatcher will invoke all callbacks that was added to it. Here’s a simple implementation.

template<typename T>
class Dispatcher;

template<typename Ret, typename ...Args>
class Dispatcher<Ret(Args...)>
{
public:
	template<Ret(*funcPtr)(Args...)>
	bool add()
	{
		return add(Delegate<Ret(Args...)>::create<funcPtr>());
	}

	template<Ret(*funcPtr)(Args...)>
	bool remove()
	{
		return remove(Delegate<Ret(Args...)>::create<funcPtr>());
	}

	template<typename T, Ret(T::*funcPtr)(Args...)>
	bool add(shared_ptr<T> obj)
	{
		return add(Delegate<Ret(Args...)>::create<T, funcPtr>(obj));
	}

	template<typename T, Ret(T::*funcPtr)(Args...)>
	bool remove(shared_ptr<T> obj)
	{
		return remove(Delegate<Ret(Args...)>::create<T, funcPtr>(obj));
	}

	template<typename T>
	bool add(shared_ptr<T> t)
	{
		return add(Delegate<Ret(Args...)>::create(t));
	}

	template<typename T>
	bool remove(shared_ptr<T> t)
	{
		return remove(Delegate<Ret(Args...)>::create(t));
	}

	void operator()(Args... args)
	{
		for (auto& delegate : delegates)
		{
			delegate(args...);
		};
	}

	bool add(Delegate<Ret(Args...)> delegate)
	{
    		// if we already added same delegate - don't add it again
		if (find(delegates.begin(), delegates.end(), delegate) != delegates.end())
		{
			return false;
		}

		delegates.push_back(delegate);

		return true;
	}

	bool remove(Delegate<Ret(Args...)> delegate)
	{
    		// remove delegate only if it exist
		auto it = find(delegates.begin(), delegates.end(), delegate);

		if (it == delegates.end())
		{
			return false;
		}

		delegates.erase(it);

		return true;
	}
private:
	vector<Delegate<Ret(Args...)>> delegates;
};

Mostly it’s a wrappers around delegate creation functions. Couple of notes here.

  • When we want to add a delegate - the new one will be created. If we want to remove it - we also need to create it to be able to compare. But that’s the price we need to pay in order to have compact delegates, without storing callback pointer and different comparing logic.

  • During call of operator() the callback function will be called. And if in this callback we remove the delegate from the dispatcher - bad things can happen. In other words in this implementation it’s possible to remove an item from the vector while iterating over this vector. This will lead to crash/corruption and additional logic needed here to avoid this situation.

We can use new Dispatcher class like this:

Dispatcher<int(int, float)> dispatcher;

dispatcher.add<&global>();

auto ptr = make_shared<UserStruct>();
dispatcher.add<UserStruct, &UserStruct::member>(ptr);

dispatcher(10, 5.0f);

dispatcher.remove<&global>();
dispatcher.remove<UserStruct, &UserStruct::member>(ptr);

Things that can be improved:

  • Maybe it would be better to return some delegate handle after adding the delegate to the dispatcher. Keeping this handle will act the same as keeping Delegate instance, but it’s more compact and simple. Later we can use the handle to remove a delegate and there will be no need to create a new Delegate instance for comparison.

  • The parameters passed to Dispatcher::operator() and Delegate::operator() are passed by copy. It would better to use perfect forwarding. But the problem is that we have typename …​Args parameter pack in class definition but usage of it - Args…​ args in the function. In other words we have a templated class but not a function. And perfect forwarding with reference collapsing rules applies only to function templates. I believe we can fix this templating a function with another parameter pack and comparing this pack with one declared in class template.

The source code can be found here.

comments powered by Disqus