std::function

std::function is an implementation of a generic function object - an object that can be used like a function while supporting passing itself by value and assigning different callables. It is the equivalent of delegate in C#. Because of std::function's primary ability, it's mainly used for callbacks - places where the user can specify what should happen when a certain action is triggered (e.g. GUI). It's essentially a replaceable virtual function that supports holding some state (usually lambda captures).

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
#include <functional>
#include <iostream>

int f1(int x, int y)
{
	return x * y;
}

struct foo
{
	int operator()(int x, int y) const
	{
		return x * y;
	}
};

struct bar
{
	int func(int x, int y) const
	{
		return x * y + z;
	}

	int z;
};

int main()
{
	// from a free function
	std::function<int(int, int)> func = f1;
	std::cout << func(2, 3) << "\n";

	// from a lambda expression
	func = [](int x, int y) { return x * y; };
	std::cout << func(2, 3) << "\n";

	// from a type with overloaded operator()
	func = foo{};
	std::cout << func(2, 3) << "\n";

	// from a class member function
	// member functions need an object to work on, thus &b
	bar b{4};
	func = std::bind(&bar::func, &b, 2, 3);
	std::cout << func(2, 3) << "\n";
	// this example could also use a capturing lambda
}

The primary features of this class template are:

  • instances are copyable

  • it can be assigned different targets (free functions, lambdas and many other callables)

  • it overloads operator() so it can be called just like a function

Internally, it allocates (statically or dynamically) a storage that holds all necessary state in order to be capable of performing the call. For free functions this simply means storing a pointer to them, for lambdas this means storing their captured state, for member functions this means storing a pointer to the object and so on...

The exact implementation uses very advanced technique - type erasure. Explaining it goes far beyond this article, especially since it requires significant knowledge in all of: templates, pointers, polymorphism implementation (how vtable works and such) and C++ language rules (concept callable, operator overloading and lifetime semantics). Additionally, efficient implementations use small buffer optimization which even further complicates the code. The greatness of the std::function is that while it's one of the hardest things to implement, it's usage is extremely simple and thus the article is focused on less skilled readers - for now I will simply call the implementation magic.

The core interface looks as follows:

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
template <typename>
class function; // no primary definition

template <typename R, typename... Args>
class function<R(Args...)> // specialization only for function types
{
	using result_type = R;

	~function();

	function();               // empty state
	function(std::nullptr_t); // empty state

	function(const function& other); // copy
	function(function&& other);      // move, noexcept in C++20

	template <typename F>
	function(F&& f); // store f and allow to call it

	function& operator=(const function& other);
	function& operator=(function&& other); // interestingly not noexcept
	function& operator=(std::nullptr_t) noexcept; // reset to empty state

	// assign a new callable target
	template <typename F>
	function& operator=(F&& f);
	template <typename F>
	function& operator=(std::reference_wrapper<F> f) noexcept;

	void swap(function& other) noexcept;

	// return whether a target has been assigned
	// allows code like if (function_object) function_object();
	explicit operator bool() const noexcept;

	// invoke the target
	// throws std::bad_function_call if there is no target
	R operator()(Args... args) const;

	// returns typeid(T) if there is a target inside of type T
	// returns typeid(void) if there is no target
	const std::type_info& target_type() const noexcept;

	// obtain a pointer to the target, if there is a target of type T
	// otherwise return a null pointer
	template <typename T>
	T* target() noexcept;
	template <typename T>
	const T* target() const noexcept;
};

// compare function object with nullptr - test whether function is empty
template <typename R, typename... Args>
bool operator==(const std::function<R(Args...)>& f, std::nullptr_t) noexcept;
template <typename R, typename... Args>
bool operator==(std::nullptr_t, const std::function<R(Args...)>& f) noexcept;
template <typename R, typename... Args>
bool operator!=(const std::function<R(Args...)>& f, std::nullptr_t) noexcept;
template <typename R, typename... Args>
bool operator!=(std::nullptr_t, const std::function<R(Args...)>& f) noexcept;

By default, the function object is empty (no target set). Calling operator() when there is no target causes std::function to throw std::bad_function_call.

Why does it throw in such case? Couldn't it just do nothing?

At first this sounds like a reasonable behavior alternative, but what when R is not void (the result is potentially passed somewhere)? Theoretically if the type was default-constructibe std::function could return a new instance of it but - what if it doesn't make sense in the given application or the type has no default constructor? I think it's much better to have a single throw-on-no-target behavior than an entire set of rules that governs what operator() does when there is no target.

The only other reasonable option would be to make it UB.

Performance

Call cost

From performance point of view, if the cost of a function call is F and the cost of a virtual function call is V + F, the cost of a std::function call is not higher than 2V + F (at least that's what I observed from my own experiments - just slightly more expensive than standard virtual call). It's a pretty efficient mechanism for something that offers a polymorphic call with the possibility of copying and replacing the target - classical virtual functions inside classes do not allow reassignment.

Allocation

std::function may allocate its storage dynamically to hold necessary data to perform the call. In case of pointer or std::reference_wrapper targets, small buffer optimization is guaranteed (no dynamic allocation, the class will use its own static buffer capable of holding a pointer).

Binds

std::bind makes very little sense even though it was added in C++11. Everything it does can be done by lambda expressions, sometimes even with better performance due to the fact that language features generally have higher potential of optimization than library code. A lot of helper binders and wrappers were already deprecated in C++11/17 and removed in C++17/20.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <functional>

void f(int n1, int n2, int n3, const int& n4, int n5)
{
	std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
}

int main()
{
	using namespace std::placeholders;

	int n = 7;
	auto b = std::bind(f, _2, 42, _1, std::cref(n), n);
	n = 10;
	b(1, 2, 3);

	// same behavior
	n = 7;
	auto l = [ncref = std::cref(n), n = n](auto a, auto b, auto /* unused */) { f(b, 42, a, ncref, n); };
	n = 10;
	l(1, 2, 3);
}

I have never found a reason to use std::bind over lambdas, so my recommendation is to use the latter.

Lifetime

Const reference

Const reference extends the lifetime of a temporary. But this is not the case when it happens through std::function call - there are multiple layers of abstraction inside (potentially multiple function calls) and thus the temporary object dies before reaching final reference.

1
2
3
4
const int& r = 42; // ok: const reference extents the lifetime of a temporary

std::function<const int&()> f = []{ return 42; };
const int& rf = f(); // undefined behavior: dangling reference

Ownership

std::function doesn't manage lifetime of objects used inside the target. It only cares about its storage that holds required information to perform the call. This means that if std::function is assigned a lambda expression with state captured by reference, the referenced state must live to the point of function object call.

1
2
3
4
5
6
7
8
std::function<int()> f;

{
	int x = 42;
	f = [&](){ return x; };
}

f(); // undefined behavior: x is already dead

Similarly, if you assign a struct with overloaded operator(), the struct will be held in std::function's storage, but anything referenced by the struct can die before invokation takes place.

Copying issues

std::function requires the target to be copyable. If you don't have C++23 and need to store a move-only callable, you can wrap it using the class template below. It won't make it copyable (will throw exception on any attempt) but will at least make it compile so that you can use the callable as long as the function object copy is not attempted.

Why such limitation exists? std::vector<T> doesn't require T to be copyable as long as vector's copy constructor is not used. Couldn't std::function go this way?

My initial thought was the answer "no, because type erasure used within its implementation requires copyable types" but after experimenting with it, I realized that std::function indeed doesn't copy the callable when the function object is not copied (at least with libstdc++ implementation, used by GCC). So sadly, I can not answer the question now. I don't even know why for C++23 it was decided to add std::move_only_function than to change specification to make std::function only require copyable types when copy constructor is called.

TODO implementation of fake_copyable

Move-only function object

Since C++23 there is std::move_only_function that allows move-only callables. The interface is identical to that of std::function, except few things:

  • const qualifier, ref-qualifiers and noexcept are a part of class template specializations and they are "forwarded" to the operator() so this class is more const-correct, ref-correct and noexcept-correct than std::function.

  • Calling operator() when there is no target is UB instead of throwing an exception.

  • No target and target_type member functions.

For reasoning, see P0288

Additional resources

note: all talks are on somewhat advanced level