A lot of times, I find myself using std::function to document APIs and prescribe specific callback signatures. The problem with std::function is its type-erasure, which makes it quite slow compared to other callback techniques. If you need ABI stability, say you are exporting a function for a DLL, you cannot use standard types anyways. They do not ascribe to any ABI standard. So in that case, you’d probably use a good old raw function pointer.

So then, to make your code readable, because you are not a monster, you pay the price of type-erasure. Yet accepting a templated callback would be completely OK, you are not publishing your function through an ABI layer. Let me illustrate with an example.

// Undocumented and permissive, but blazing fast. +1 wtf during code review.
template <class Func>
void call_user_callback(Func);

// Alternative, readable and documented but slow :(
void call_user_callback(std::function<void(int)>);

// ABI published callback, for completeness.
void call_user_callback(void(*)(int, void*));

What I truly want is a fast callback mechanism that is clear and strict. I need to prevent any mishaps with auto vs. auto& and I need the signature to be instantly readable in a header.

Some may suggest one of the function_view implementations that have been floating around. But they use type-erasure, so they are slow. Furthermore, they are the unsafest thing I’ve seen in a while, so no never. The SG14 has a safer approach with inplace_function, but it still requires type-erasure.

Today, I present an alternative that is both strict and documented, to be used when your API can accept template parameters.

A Safer Callback Mechanism

Here is what your API will look like.

// Callee
template <class Func>
void call_user_callback(callback<Func, void(int)>);

You both accept a template parameter and provide a function signature the user callback must fulfill. This won’t compile if there is a screw up with references. It also makes the signature readable and documented. Of course, you won’t be able to use auto anymore. Furthermore, to keep the implementation human readable, this supports functions and function objects that define operator(), but not any other member function pointer.

To call the API, the user must construct a callback object. After days of fighting with user deduction guides, I couldn’t get it too implicitly construct from lambdas. So the caller will have to construct an object manually.

// Caller, c++17
call_user_callback(callback{ [](int){} });

// Caller, c++14
call_user_callback(make_callback([](int){}));

In terms of performance, this beats raw pointers on many occasions since the compilers aggressively inlines the callbacks. Measuring the actual time of a call is quite difficult in that sense.

If you think this is useful to you, read on.

Implementation

First we need a type for the API signature. That is, the type you will accept in your function. First we start by declaring a templated struct with 2 template parameters. One for the invocable type and one for the signature.

#include <utility>
#include <type_traits>

template <class, class = void>
struct callback;

template <class Func, class Ret, class... Args>
struct callback<Func, Ret(Args...)> {
	using func_t = Func;

	constexpr callback(const Func& func)
			: _func(func) {
	}
	constexpr callback(Func&& func)
			: _func(std::move(func)) {
	}

	constexpr Ret operator()(Args... args) const {
		return _func(std::forward<Args>(args)...);
	}

private:
	Func _func;
};

If you’ve never implemented a std::function like object before, this may look alien. Ultimately, it is a trick so you can have pretty declarations like void(int, bla, bla). Notice the Ret and Args... parameters being concatenated in the template specialization Ret(Args...).

The struct doesn’t do much else. It holds a Func object and implements the operator(). We forward the arguments inside the paren operator in case the signature accepts rvalue references.

Now, we must create a caller object which can be implicitly converted to our API object. For this, we will use slicing. Here is what the caller struct will look like, details on the inheritance will come shortly.

template <class Func, class>
struct callback : detail::callback_selector<Func> {
	using func_t = Func;
	using base = detail::callback_selector<Func>;

	constexpr callback(const Func& func)
			: base(func) {
	}
	constexpr callback(Func&& func)
			: base(std::move(func)) {
	}
};

This is the implementation of our original callback struct forward declaration, rather than a specialization. All it does is forward the accepted callback to base classes which will do some magic. That’s it, really.

At this point, we must find a way to extract the Func return type and arguments, to slice into the first callback specialization. It isn’t a simple deal, since object lambdas decay into Ret(T::*)(Args...) const, rather than the simple Ret(Args...). Furthermore, we must get the address of the operator paren if Func is an object. So the first layer of processing will be to select between specializations for object types vs. function pointer types. Here goes.

namespace detail {

// Helper for is_detected.
template <class...>
using void_t = void;

// More info : https://en.cppreference.com/w/cpp/experimental/is_detected
template <template <class...> class Op, class = void, class...>
struct is_detected : std::false_type {};
template <template <class...> class Op, class... Args>
struct is_detected<Op, void_t<Op<Args...>>, Args...> : std::true_type {
};

template <template <class...> class Op, class... Args>
inline constexpr bool is_detected_v
		= is_detected<Op, void, Args...>::value;


// Detects if a type T has operator()
template <class T>
using has_operator_paren = decltype(&T::operator());

// Declaration to enable_if in specialization.
template <class, class = void>
struct callback_selector;

// Calls &Func::operator() if the provided Func has an operator paren.
// Forwards that to callback_base.
template <class Func>
struct callback_selector<Func,
		std::enable_if_t<is_detected_v<has_operator_paren, Func>>>
		: callback_base<Func, decltype(&Func::operator())> {
	using base = callback_base<Func, decltype(&Func::operator())>;
	using base::base;
};

// Forwards Func directly to callback_base.
template <class Func>
struct callback_selector<Func,
		std::enable_if_t<!is_detected_v<has_operator_paren, Func>>>
		: callback_base<Func, Func> {
	using base = callback_base<Func, Func>;
	using base::base;
};

} // namespace detail

Alright, things are getting interesting! Or horrible, depending on your point of view. I won’t go into details on the is_detected idiom. You can read-up the cppreference article on it, there are also some good talks about the idiom. All I’ll say is thank you Walter Brown, it is indeed very useful.

The callback_selector chooses whether to call &Func::operator() or not on the provided template parameter. It inherits callback_base and forwards a function pointer type to it. It also inherits the callback_base constructors, which will forward the actual function pointer or object to the base class.

Lets take a look at callback_base if we dare.

namespace detail {

template <class, class>
struct callback_base;

template <class Func, class Ret, class... Args>
struct callback_base<Func, Ret(Args...)> : callback<Func, Ret(Args...)> {
	using base = callback<Func, Ret(Args...)>;
	using base::base;
};

template <class Func, class Ret, class... Args>
struct callback_base<Func, Ret (*)(Args...)> : callback<Func, Ret(Args...)> {
	using base = callback<Func, Ret(Args...)>;
	using base::base;
};

template <class Func, class Ret, class T, class... Args>
struct callback_base<Func, Ret (T::*)(Args...)> : callback<Func, Ret(Args...)> {
	using base = callback<Func, Ret(Args...)>;
	using base::base;
};

template <class Func, class Ret, class T, class... Args>
struct callback_base<Func, Ret (T::*)(Args...) const>
		: callback<Func, Ret(Args...)> {
	using base = callback<Func, Ret(Args...)>;
	using base::base;
};

} // namespace detail

Here, we extract the information we need about the user provided function pointer / function object. When providing a lambda, the last specialization will be taken. With all of the information we need extracted from our invocable, we can the inherit our original callback struct. We provide it the function return type and arguments.

Again, we inherit our base class constructors, which will forward the templated callback to the base class.

And that’s it. With slicing, the compiler will accept our caller specialization and slice all the way to the “signature” callback type. If there are any mismatches in the signature, it won’t compile. Since we are storing the invocable as-is, the call and inlining capabilities are impressive.

To sum up again, callback<Func> inherits callback_selector which inherits callback_base which finally inherits our callback<Func, Ret(Args...)> declaration object. Slicing does the “implicit conversion” work when we call the API.

That’s it for me today, stay safe and don’t be a monster, make your APIs readable ;)

Update : Programmer? Yeah! has created a short video series on this post. In the first video, he goes into great detail about the various callback mechanisms of C++ and thoroughly explains details I glided over. In his second video, he criticizes the implementation and points out valid shortcomings with it. Good watches if you want more details about the problem I was trying to fix.