A lambda is shorthand for a small class with an operator(). Once you see the
class the compiler writes for you, captures stop being magic, and the two bugs
that matter — a closure outliving what it refers to, and paying for type
erasure you didn't need — become easy to spot.
1. What a lambda really is
#include <print>
int main() {
int threshold = 10;
auto above = [threshold](int x) { return x > threshold; };
std::println("{} {}", above(5), above(12));
// Roughly what the compiler generates:
struct Above {
int threshold; // one member per capture
bool operator()(int x) const { return x > threshold; }
};
Above same{threshold};
std::println("{} {}", same(5), same(12));
}Each lambda expression has its own unique, unnamed closure type. That's why
you store lambdas with auto (or pass them to templates): two lambdas with
identical text still have different types.
Notice the const on operator(): by default a lambda can't modify its
by-value captures. mutable removes that const.
2. Captures
| Capture | Member stored in the closure | Lifetime risk |
|---|---|---|
[x] |
a copy of x, made when the lambda is created |
none |
[&x] |
a reference to x |
dangles if the closure outlives x |
[y = expr] |
init-capture: a new member initialised from expr (can move: [p = std::move(ptr)]) |
none for by-value |
[this] |
the this pointer |
dangles if the closure outlives the object |
[*this] |
a copy of the whole object | none |
[=] / [&] |
everything used, by value / by reference | as above, per variable |
Prefer naming what you capture. [=] in a member function captures the this
pointer, not a copy of the object (C++20 deprecates that implicit capture),
which surprises people.
#include <memory>
#include <print>
int main() {
int x = 1;
auto by_value = [x] { return x; };
auto by_ref = [&x] { return x; };
x = 2;
std::println("by value: {}, by reference: {}", by_value(), by_ref());
auto counter = [n = 0]() mutable { return ++n; }; // init-capture + mutable
counter();
std::println("counter: {}", counter());
auto p = std::make_unique<int>(42);
auto owner = [p = std::move(p)] { return *p; }; // the closure now owns the int
std::println("owner: {}, p is {}", owner(), p ? "set" : "empty");
}x = 2: by_value still holds its own 1, while by_ref follows the reference to the variable and sees 2.The dangling closure
A closure that captures by reference is only valid while the referenced objects live. Returning such a closure from a function is the classic bug:
auto make_counter(int start) {
int next = start;
return [&next] { return next++; }; // next dies when make_counter returns
}make_counter's frame is popped when it returns, but the closure kept a reference to next inside it. The next call to the closure reads and writes memory that may already belong to another function.AddressSanitizer reports it as a stack-use-after-scope/return. The fix is to
capture by value and make the lambda mutable so it can update its own
copy: [next = start]() mutable { return next++; }.
Rule of thumb: by-reference captures are fine for lambdas that are used immediately (passed to an algorithm, called right away). Anything stored, returned, or handed to another thread should capture by value.
3. Copying a closure copies its state
A closure is an object; copying it copies its members. Two copies of a
mutable counter count independently:
#include <print>
int main() {
auto c = [n = 0]() mutable { return ++n; };
c();
auto d = c; // d starts with n == 1, like c
std::println("{} {}", c(), d()); // 2 2
}If copies should share state, share it explicitly — e.g. capture a
std::shared_ptr to the state.
4. Generic lambdas, template lambdas, and recursion
#include <print>
#include <string>
#include <vector>
int main() {
auto twice = [](auto x) { return x + x; }; // generic: a template operator()
std::println("{} {}", twice(21), twice(std::string("ab")));
auto same_type_sum = []<class T>(T a, T b) { return a + b; }; // template lambda: names T
std::println("{}", same_type_sum(1.5, 2.25));
auto first_or = []<class T>(const std::vector<T>& v, T fallback) {
return v.empty() ? fallback : v.front();
};
std::println("{}", first_or(std::vector<int>{}, -1));
// C++23 deducing this: the lambda receives itself as a parameter, so it can recurse.
auto fib = [](this auto self, int n) -> long long {
return n < 2 ? n : self(n - 1) + self(n - 2);
};
std::println("fib(30) = {}", fib(30));
}Before deducing this, recursive lambdas needed std::function (slow,
allocates) or a "pass yourself" parameter. A lambda can't capture itself by
name in its own initialiser.
Immediately invoked lambdas
A lambda called on the spot is the neat way to initialise a const variable
with logic:
#include <print>
#include <string_view>
int main() {
std::string_view mode = "fast";
const int iterations = [&] {
if (mode == "fast") return 10;
if (mode == "thorough") return 1000;
return 100;
}();
std::println("{}", iterations);
}5. Storing and passing callables
There are four ways to accept "something callable" in an API. They trade flexibility against cost:
| Parameter type | Owns the callable? | Allocates? | Copyable? | Use when |
|---|---|---|---|---|
template <class F> void f(F fn) / std::invocable auto fn |
yes (a copy) | no | as F | header code, hot paths; every callable type gets its own instantiation |
std::function_ref<R(Args...)> (C++26) |
no, just refers | never | yes (cheap) | the callee calls it during the call and doesn't keep it |
std::function<R(Args...)> |
yes | may (large captures) | requires a copyable callable | stored callbacks that must be copyable |
std::move_only_function<R(Args...)> (C++23) |
yes | may | no (move-only) | stored callbacks; accepts move-only lambdas (e.g. capturing unique_ptr) |
std::function and std::move_only_function use type erasure: they store
any callable behind one type, allocating on the heap when the callable is too
big for their small internal buffer. std::function_ref stores just a pointer
to the callable plus a pointer to a call thunk — it never allocates, but it
must not outlive the callable it refers to:
#include <functional>
#include <memory>
#include <print>
// The callee only calls `attempt` while running: function_ref is the right type.
bool retry(std::function_ref<bool()> attempt, int times) {
for (int i = 0; i < times; ++i)
if (attempt()) return true;
return false;
}
int main() {
int tries = 0;
bool ok = retry([&] { return ++tries == 3; }, 5); // fine: the lambda lives until retry returns
std::println("ok={} after {} tries", ok, tries);
std::move_only_function<int()> stored = [p = std::make_unique<int>(7)] { return *p; };
std::println("stored() = {}", stored());
// std::function<int()> copyable = [p = std::make_unique<int>(7)] { return *p; }; // error: not copyable
}function_ref is two pointers into the caller's world (16 bytes, never allocates). std::function (32 bytes in libstdc++) and move_only_function (40) own a copy of the callable — inline if it fits their small buffer, otherwise on the heap.std::function_ref<int()> r = [] { return 1; }; // the temporary lambda dies at the ';'
r(); // dangling: undefined behaviour6. std::invoke and member pointers
Algorithms and wrappers call things with std::invoke, which also accepts
pointers to members — so &Point::x works as a projection or a callable:
#include <algorithm>
#include <functional>
#include <print>
#include <vector>
struct Point {
int x, y;
int sum() const { return x + y; }
};
int main() {
Point p{3, 4};
std::println("{} {}", std::invoke(&Point::x, p), std::invoke(&Point::sum, p));
std::vector<Point> pts{{5, 1}, {2, 9}, {4, 4}};
std::ranges::sort(pts, {}, &Point::x); // projection = member pointer
std::println("{} {} {}", pts[0].x, pts[1].x, pts[2].x);
}If you write your own higher-order functions, call user callables with
std::invoke(f, args...) rather than f(args...) so member pointers work too.
Worked example: composing callables
f and g, so it can outlive the arguments it was built from.compose(f, g) returns a callable computing f(g(args...)). It must own f
and g (the result may outlive the arguments), so it captures them by move:
#include <functional>
#include <print>
#include <string>
#include <utility>
template <class F, class G>
auto compose(F f, G g) {
return [f = std::move(f), g = std::move(g)]<class... A>(A&&... args) mutable {
return std::invoke(f, std::invoke(g, std::forward<A>(args)...));
};
}
int main() {
auto add = [](int a, int b) { return a + b; };
auto show = [](int v) { return "<" + std::to_string(v) + ">"; };
auto add_then_show = compose(show, add);
std::println("{}", add_then_show(2, 3)); // <5>
}- Captures by move so the result owns its pieces and can be returned.
mutableso stateful callables (like a counter) can update.- Template parameter pack +
std::forwardpassesg's arguments through unchanged;std::invokeletsforgbe member pointers.
Faded example — your turn
Write make_accumulator(start): each call acc(x) adds x to a running
total and returns the new total. What do you capture, how, and does the lambda
need mutable? What happens to two copies of the accumulator?
auto make_accumulator(int start) {
return /* ??? */;
}Answer
return [total = start](int x) mutable { return total += x; }; — capture the
running total by value (an init-capture), since the closure outlives
make_accumulator, and mark it mutable so operator() may change it. Copies
have independent totals; to share one total, capture a std::shared_ptr<int>.
Summary
- A lambda is an object of a unique class; captures are its members.
- Capture by value for closures that are stored or returned; by reference only
for immediate use.
mutablelets the closure change its own copies. - Template lambdas (
[]<class T>) name their types; deducingthismakes recursion easy. - APIs: a template or
function_reffor "call during this call";move_only_function(orfunctionif copies are needed) for "store and call later".function_refnever allocates but must not outlive its target. - Call user callables with
std::invokeso member pointers work.