Lesson 4 of 5
Every lambda has its own type, which no one can write (lesson 14.1). So how do you store one in a
class member, or a std::vector of callbacks?
std::function<Signature> holds any callable matching a signature — at a price this lesson
measures.
What it does
// std::function<int(int)> f;
//
// f = [](int x) { return x * 2; }; // a lambda
// f = doubler; // a function
// f = Multiplier{2}; // a function object
// f = [n = 2](int x) { return x * n; }; // a capturing lambda
//
// All four fit, because std::function stores anything callable as int(int).
That is type erasure: the concrete type is hidden behind a uniform interface. It is what makes
callbacks, event handlers, and std::vector<std::function<void()>> possible.
The cost, measured
#include <chrono>
#include <functional>
#include <iostream>
#include <vector>
template <typename F>
long long time_us(F f) {
auto t0 = std::chrono::steady_clock::now();
f();
auto t1 = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
}
// Takes the lambda's own type: the call can be inlined.
template <typename F>
long long sum_with_template(const std::vector<int>& v, F transform) {
long long total = 0;
for (int x : v) total += transform(x);
return total;
}
// Takes std::function: an indirect call the compiler usually cannot inline.
long long sum_with_function(const std::vector<int>& v, const std::function<int(int)>& transform) {
long long total = 0;
for (int x : v) total += transform(x);
return total;
}
// A plain function pointer, for comparison.
long long sum_with_pointer(const std::vector<int>& v, int (*transform)(int)) {
long long total = 0;
for (int x : v) total += transform(x);
return total;
}
int doubler(int x) { return x * 2; }
int main() {
std::vector<int> v(5000000);
for (std::size_t i = 0; i < v.size(); ++i) v[i] = static_cast<int>(i % 100);
auto lambda = [](int x) { return x * 2; };
std::function<int(int)> fn = lambda;
long long sink = 0;
auto t_tmpl = time_us([&]{ sink += sum_with_template(v, lambda); });
auto t_fn = time_us([&]{ sink += sum_with_function(v, fn); });
auto t_ptr = time_us([&]{ sink += sum_with_pointer(v, doubler); });
std::cout << "template parameter: " << t_tmpl << " us\n";
std::cout << "std::function: " << t_fn << " us\n";
std::cout << "function pointer: " << t_ptr << " us\n";
std::cout << "(sink " << sink << ")\n";
std::cout << "\nsizeof(lambda): " << sizeof(lambda) << "\n";
std::cout << "sizeof(std::function<int(int)>): " << sizeof(fn) << "\n";
std::cout << "sizeof(int(*)(int)): " << sizeof(&doubler) << "\n";
return 0;
}
A representative run, compiled -O2:
template parameter: 1543 us
std::function: 6288 us
function pointer: 6217 us
(sink 1485000000)
sizeof(lambda): 1
sizeof(std::function<int(int)>): 32
sizeof(int(*)(int)): 8
The template version was 4× faster. Passing the lambda's own type lets the compiler see the body and inline it — the multiply happens directly in the loop, with no call at all.
std::function and the raw function pointer performed the same. That is the honest result and
it is informative: the cost is not std::function being bloated, it is the indirect call. Any
mechanism that hides which function will run pays it.
Sizes: the lambda is 1 byte, the function pointer 8, and std::function 32 — a small buffer
plus bookkeeping.
When it allocates
std::function has a small-buffer optimisation. Counting heap allocations with a replaced
operator new finds the boundary exactly:
#include <cstddef>
#include <cstdlib>
#include <functional>
#include <iostream>
static int allocations = 0;
void* operator new(std::size_t n) { ++allocations; return std::malloc(n); }
void operator delete(void* p) noexcept { std::free(p); }
void operator delete(void* p, std::size_t) noexcept { std::free(p); }
template <int N>
struct Blob { char data[N]; };
template <int N>
void probe() {
Blob<N> blob{};
allocations = 0;
std::function<void()> f = [blob] { (void)blob; };
std::cout << " capture of " << N << " bytes: " << allocations << " allocation(s)\n";
}
int main() {
std::cout << "sizeof(std::function<void()>) = " << sizeof(std::function<void()>) << "\n";
probe<8>(); probe<12>(); probe<16>(); probe<17>(); probe<24>(); probe<32>();
return 0;
}
sizeof(std::function<void()>) = 32
capture of 8 bytes: 0 allocation(s)
capture of 12 bytes: 0 allocation(s)
capture of 16 bytes: 0 allocation(s)
capture of 17 bytes: 1 allocation(s)
capture of 24 bytes: 1 allocation(s)
capture of 32 bytes: 1 allocation(s)
16 bytes is the exact threshold in libstdc++. Up to that, the closure lives inside the
std::function; one byte more and it goes on the heap.
Sixteen bytes is two pointers, or two long longs, or a std::string — no, a std::string is 32,
so capturing one by value allocates. That is easy to do by accident:
// std::function<void()> f = [name] { use(name); }; // allocates: string is 32 bytes
// std::function<void()> f = [&name] { use(name); }; // no allocation — but see lesson 14.2
The number is not portable. It is 16 in libstdc++ and different in libc++ and MSVC, so do not build a design on it — but do know that a callback stored per event, in a loop, may be allocating.
The alternative: a template parameter
// std::function — one compiled function, indirect call, may allocate.
// void apply(const std::vector<int>& v, const std::function<int(int)>& f);
//
// Template — one instantiation per callable, inlinable, no allocation.
// template <typename F>
// void apply(const std::vector<int>& v, F f);
//
// C++20 concept — same as the template, with a readable error.
// void apply(const std::vector<int>& v, std::invocable<int> auto f);
The template is faster and the std::function is more flexible, and the choice is decided by when
you know which callable it is.
| | Template parameter | std::function |
|---|---|---|
| Call | inlinable | indirect |
| Allocation | never | if the closure exceeds the buffer |
| Stored in a member | no (type is unnamable) | yes |
| Stored in a container | no | yes |
| Reassigned at run time | no | yes |
| Compiled | once per callable type | once |
| Must be in a header | yes | no |
The last row matters in a large codebase: a template must be visible at every call site
(lesson 12.1), while a std::function parameter lets the implementation live in a .cpp. That is a
real reason to accept the indirect call.
When to use which
Use a template parameter for a callable you pass down and call immediately — algorithms,
for_each-style helpers, anything in a hot loop. This is the default.
Use std::function when the callable must be stored: a class member, a std::vector of
handlers, a registry, or an API boundary compiled separately.
Use a plain function pointer only for a C API, or when there is genuinely no state. It is no
faster than std::function — the measurement above shows the two matching — and it cannot hold a
capturing lambda at all.
A capture-less lambda converts implicitly to a function pointer, which is how you pass one to a C callback:
// void register_handler(void (*cb)(int));
// register_handler([](int x) { std::cout << x; }); // fine, no captures
// register_handler([&](int x) { std::cout << n + x; }); // error: captures
std::move_only_function (C++23)
std::function requires its target to be copyable, so a lambda capturing a std::unique_ptr
does not fit. C++23 adds std::move_only_function, which does not:
// std::function<void()> f = [p = std::make_unique<int>(1)] { }; // error
// std::move_only_function<void()> g = [p = std::make_unique<int>(1)] { }; // fine
GCC 13 supports it with -std=c++23. Before that, the workaround is a captured std::shared_ptr,
which is copyable — as lesson 14.2's exercise did.
Try it
Build a small event system: register handlers by name, then fire an event and run all of them.
Then write the same thing as a template and say which parts of the design force std::function.
Checkpoint
std::function<Sig>holds any callable matching a signature — type erasure.- Measured: a template parameter was 4× faster, because the call inlines.
std::functionand a raw function pointer performed the same — the cost is the indirect call.sizeof(std::function<int(int)>)is 32; the lambda itself was 1 byte.- libstdc++ stores closures up to 16 bytes inline; 17 allocates. The number is not portable.
- Capturing a
std::stringby value (32 bytes) allocates on every construction. - Use a template parameter for callables passed down and called immediately.
- Use
std::functionwhen the callable must be stored, reassigned, or crossed over an ABI. - A template must live in a header; a
std::functionparameter lets the body stay in a.cpp. - A capture-less lambda converts to a function pointer; a capturing one does not.
std::functionrequires a copyable target; C++23'sstd::move_only_functiondoes not.
Questions about this lesson
Log in to ask a question.