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);