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 requireT
to be copyable as long as vector's copy constructor is not used. Couldn'tstd::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 andnoexcept
are a part of class template specializations and they are "forwarded" to theoperator()
so this class is more const-correct, ref-correct and noexcept-correct thanstd::function
.Calling
operator()
when there is no target is UB instead of throwing an exception.No
target
andtarget_type
member functions.
For reasoning, see P0288
Additional resources
note: all talks are on somewhat advanced level