Who calls Who? Callbacks in C++11

8Mar 2015

Who calls Who? Callbacks in C++11

A simple function encapsulates some functionality. It takes a set of arguments, processes them, and returns a value. As such, a function is reusable, but not very flexible. Whatever it does is encoded in the function itself. Other than passing different arguments, the caller does not have any control over its functionality.

Many languages support the notion of functions as first class objects. That means that functions can be treated like any other object in that language. Therefore, functions can be passed as arguments to other functions, which in turn call the passed functions as part of their processing. This gives the caller control over what is being done in a function. Typical applications of this pattern are functions like map, reduce, or filter.

The same pattern is also used in C++, for example in the C++ algorithm functions. However, C++ does not treat functions as first class objects. But since C++11, we have a function wrapper called std::function. It can store, copy, and invoke anything callable.

In this article, we want to discuss how std::function can be used as a callback in various scenarios.

The complete source code with more examples and documentation is available on Github.

Invoke a callback

std::function can store and invoke anything callable. Callable in C++ are functions, lambda expressions, std::bind expressions, function objects, and pointers to member functions.

The callable wrapped in std::function is called the target. Invoking the target is done with operator().

The following code demonstrates how to store a bind expression and invoke it.

using cb1_t = std::function<void()>;
void foo1() { ... }
void foo2(int i) { ... }

cb1_t f1 = std::bind(&foo1);
cb1_t f2 = std::bind(&foo2, 5);

f1(); // Invoke foo1()
f2(); // Invoke foo2(5)

See example 1 on Github.

Store a callback

Invoking a callback is good, but often we need to store a list of callbacks for later invocation. Typically, this type of code is used to register clients that are notified on an event.

The code is the same as in the previous example, except that we store the callbacks in a std::vector. Note that we cannot store different types in a vector. Later we will see a possible way to work around this limitation. However, for many tasks one type of callback is sufficient. Also note that a bind expression evaluates to the same type when arguments are fixed or specified through std::ref or std::cref. This allows for some flexibility without any additional complexity.

using cb1_t = std::function<void()>;
std::vector<cb1_t> callbacks;

void foo1() { ... }
void foo2(int i) { ... }

cb1_t f1 = std::bind(&foo1);
callbacks.push_back(f1);

int n = 15;
cb1_t f2 = std::bind(&foo2, std::ref(n));
callbacks.push_back(f2);

// Invoke the functions
for(auto& fun : callbacks) {
	fun();
}

See example 2 on Github.

Wrapper Functions

The next example shows how std::functon can be passed as argument to a function. Here we create a wrapper function to call the passed function. The example shows how such a function can be overloaded to cope with different types of arguments. The code for foo1 is the same as before and is omitted here.

Note that we always produce an std::function, even though in some cases we could invoke the target directly. Whether this is required depends on the use case. If all the function does is invoking the target, then directly doing it is more efficient. The reason is that std::function does have some overhead, because it is a polymorphic class.

// Wrapper function for generic callable object without arguments.
// Delegates to the std::function call.
template<typename R>
void call(R f(void))
{
	call(std::function<R(void)>(f));
}

// Wrapper function with std::function without arguments.
template<typename R>
void call(std::function<R(void)> f)
{
	f();
}

// ... aliases and function foo1 like before

// Call function 1 through wrapper with generic argument.
call(&foo1);

// Call function 1 through wrapper with function argument.
cb1_t f1 = std::bind(&foo1);
call(f1);

The example here only shows the wrapper function that takes no arguments. More examples including a wrapper function with arguments can be found in example 3 on Github.

Heterogeneous callback types

Using the examples from above, and expanding on them, allows us to finally create a solution to store callbacks of different types.

The basic idea is to store a common type in a collection. The concrete callbacks are derived from this type and wrapped in a std::unique_ptr. Working with a unique_ptr is easier in this case because it already implements move operations. For this example, we use a std::map as a collection. The index is the typeid of the callback.

The full source code is available in example 4 on Github.

// The base type that is stored in the collection.
struct Func_t {
	virtual ~Func_t() = default;
};
// The map that stores the callbacks.
std::map<std::type_index, std::unique_ptr<Func_t>> callbacks;

The callback type derives from this base type and is parametrized with the user defined arguments. We don’t parametrize the return value, which is also possible if required.

template<typename ...A>
struct Cb_t : public Func_t {
	using cb = std::function<void(A...)>;
	cb callback;
	Cb_t(cb p_callback) : callback(p_callback) {}
};

Given the same foo1 and foo2 functions as before, we can specify our callbacks and store them in the map.

using func1 = Cb_t<>;
std::unique_ptr<func1> f1(new func1(&foo1));
using func2 = Cb_t<int>;
std::unique_ptr<func2> f2(new func2(&foo2));

// Add the to the map.
std::type_index index1(typeid(f1));
std::type_index index2(typeid(f2));
callbacks.insert(callbacks_t::value_type(index1, std::move(f1)));
callbacks.insert(callbacks_t::value_type(index2, std::move(f2)));

In order to actually invoke a callback, we must be able to reconstruct the type of the callback. This is done with a wrapper function, similar to the one we saw earlier. Only we also pass the index that identifies the callback in the collection and the arguments will be moved.

template<typename ...A>
void call(std::type_index index, A&& ... args)
{
	using func_t = Cb_t<A...>;
	using cb_t = std::function<void(A...)>;
	const Func_t& base = *callbacks[index];
	const cb_t& fun = static_cast<const func_t&>(base).callback;
	fun(std::forward<A>(args)...);
}

Finally, we can invoke the callback through the wrapper function, using the index created above.

call(index1);
call(index2, 5);

Tags: programming