C++ / LAMBDAS AND CALLABLE OBJECTS
std::function and type-erased callbacks
Store any callable behind one signature with std::function, understand the type erasure and copying it does, and know when a template parameter is better.
What you will learn
- Store function pointers, functors, and lambdas in one std::function<R(Args...)>
- Explain why lambdas must be erased before they can share a container or a member
- Guard calls with operator bool to avoid std::bad_function_call on an empty wrapper
- Pick a template parameter over std::function when no runtime erasure is needed
Understanding std::function and type-erased callbacks
Every lambda expression creates a brand-new, unnamed class type, so two lambdas with byte-identical bodies are still unrelated types. That makes them impossible to name in a declaration: there is no spelling for a vector element, a data member, or a function parameter that means "any callable taking int and returning int". std::function<int(int)> is that missing name. Its type depends only on the signature you write, and it accepts a function pointer, an object with operator(), or any lambda that can be invoked with an int and yields something convertible to int.
It manages this by type erasure. When you assign a callable, std::function copies or moves it into its own storage and records, through function pointers or a vtable, how to invoke, copy, and destroy that one specific type. Nothing about the target's type appears in the wrapper's type, which is exactly what lets a single declaration hold many different callables over its lifetime. The mental model is an owned pointer to an abstract base with a virtual operator(), except that copying duplicates the target instead of sharing it, and small targets usually fit in an internal buffer rather than the heap.
Erasure has a price. The call goes through an indirection the optimizer normally cannot see through, so the target body is not inlined, and a target too large for the internal buffer costs a heap allocation when the wrapper is built. Use a template parameter when the callable's type is known at the call site and you only need to forward it; reach for std::function when the type genuinely has to disappear, as with container elements, a member assigned later, an interface compiled separately, or a slot reassigned at runtime. Erasure also adds a state a raw callable does not have, empty, which is why invoking an unset wrapper throws std::bad_function_call.
<functional>
<iostream>
<vector>
int triple(int x) { return 3 * x; }
struct AddN {
int n;
int operator()(int x) const { return x + n; }
};
int main() {
int offset = 100;
std::vector<std::function<int(int)>> ops;
ops.push_back(triple); // function pointer
ops.push_back(AddN{7}); // class with operator()
ops.push_back([offset](int x) { return x - offset; }); // capturing lambda
for (const std::function<int(int)>& op : ops)
std::cout << op(10) << '\n';
std::function<int(int)> f; // empty: holds nothing
std::cout << std::boolalpha << "empty holds a target: " << static_cast<bool>(f) << '\n';
try {
f(0);
} catch (const std::bad_function_call&) {
std::cout << "calling it threw std::bad_function_call\n";
}
f = AddN{1};
std::cout << "after assignment: " << static_cast<bool>(f)
<< ", f(41) = " << f(41) << '\n';
}
std::function trades a callable's real type for a fixed signature by owning a copy of it and invoking it through an indirection.
Worked examples
The signature is a contract, not a type match
Shows that the target's parameters and return type only have to be convertible to the wrapper's signature.
<functional>
<iostream>
int main() {
// target takes long and returns int; the wrapper promises void(int)
std::function<void(int)> sink = [](long v) -> int {
std::cout << "sink got " << v << '\n';
return 1; // the wrapper discards this
};
sink(42);
std::function<double(double, double)> add = [](int a, int b) { return a + b; };
std::cout << "add(3.9, 2.9) = " << add(3.9, 2.9) << '\n';
}
Example explained
Line 1std::function only requires the target to be invocable with the listed argument types, so the int 42 converts to long on the way in.
Line 2Because the wrapper's return type is void, a target returning int is still accepted and its result is thrown away.
Line 3add(3.9, 2.9) truncates both arguments to int before the lambda runs, so 3 + 2 = 5 comes back and is converted to double.
Line 4Nothing here is a compile error, which is why an erased signature can silently narrow values you pass through it.
Copies of the wrapper copy the callable
Demonstrates that std::function owns its target by value, and how std::ref opts out of that copy.
<functional>
<iostream>
struct Counter {
int calls = 0;
int operator()() { return ++calls; }
};
int main() {
std::function<int()> a = Counter{}; // the Counter is copied into a
a();
a(); // a's own count is now 2
std::function<int()> b = a; // copies the stored Counter, state included
std::cout << "b: " << b() << '\n';
std::cout << "a: " << a() << '\n';
Counter c;
std::function<int()> d = std::ref(c); // stores a reference_wrapper, not a copy
d();
d();
std::cout << "c.calls: " << c.calls << '\n';
}
Example explained
Line 1Assigning Counter{} copies the object into the wrapper's storage, so the count lives inside a and the temporary is gone.
Line 2Copying a std::function deep-copies its target, so b starts from a's count of 2 and both wrappers print 3 while advancing separately.
Line 3std::ref(c) stores a reference_wrapper holding a pointer to c, so calls through d mutate the original and c.calls ends at 2.
Line 4Note that std::function::operator() is const yet still invokes Counter's non-const operator().
Important notes
std::function::operator() is const but invokes the stored target as a non-const lvalue, so a const std::function can still mutate the state it owns; const here promises nothing about thread safety.
Since C++23, std::move_only_function<R(Args...)> holds move-only targets and honours const and ref qualifiers in the signature; std::function itself still requires a copyable target.
Common mistakes
Taking a std::function parameter for a callback invoked millions of times inside the function: the target can never be inlined and building the wrapper may allocate, while a template parameter compiles to a direct, inlinable call.
Calling a default-constructed member such as on_event without checking it; an unset std::function is not a harmless no-op, it throws std::bad_function_call at runtime.
Expecting the wrapper to refer to your object: assigning a functor copies it, so mutations made through the wrapper never reach the original, and a lambda capturing a std::unique_ptr will not compile at all because the target must be copy-constructible.
Try it yourself
Change, predict, then run
Build a std::vector<std::function<int(int)>> holding a free function, a functor, and a lambda that captures a local multiplier, then feed 5 through all three in order and print each intermediate result. Add one default-constructed element and make the loop skip it with a bool check instead of throwing.
Open the C++ workspaceCheck your understanding
A hot loop calling a std::function<int(int)> parameter runs several times slower than the same loop with a template parameter for the callable. What is the main reason?
- The target is reached through erased dispatch pointers the compiler cannot see through, so its body is never inlined.
- std::function re-converts each argument on every call, and those conversions dominate the loop.
- std::function heap-allocates storage for the arguments on each call.
- The template version calls the target directly, while std::function must copy the target before each call.
Show answer
With a template parameter the exact callable type is part of the instantiation, so the body can be inlined into the loop and often folded into a couple of instructions; through std::function the call reaches the target via stored pointers, which blocks that. Option 3 is tempting because std::function really can allocate, but that happens at most once when the wrapper is constructed, and usually not at all thanks to the small-object buffer, never per call.