Lambda Expressions
Anonymous closures defined inline — syntactic sugar for a local struct with operator(). Core tool for callbacks, algorithms, and initialization patterns.
Lambda Expressionsince C++11A lambda expression creates a closure object of a unique, unnamed type — a compiler-generated struct with a matching operator() — that captures variables from the enclosing scope according to its capture list.
Overview
Every lambda expression produces a distinct closure type. Two syntactically identical lambdas still have different types — you cannot assign one to the other. The closure type's operator() is const by default (preventing mutation of captured values); mutable removes that const.
Captures are taken at lambda creation, not at call time. A by-value capture snapshots the current value; a by-reference capture aliases the original and is only safe if the lambda doesn't outlive it.
| Standard | What was added |
|---|---|
| C++11 | Lambdas, explicit captures, mutable, trailing return type |
| C++14 | Generic lambdas (auto params), init captures [x = expr], default param values |
| C++17 | constexpr lambdas (implicit when eligible), *this capture |
| C++20 | Template lambdas []<typename T>, default-constructible stateless lambdas, pack expansion in init capture [...xs = move(xs)], [=] implicit this capture deprecated |
| C++23 | Explicit object parameter (this auto& self) — recursive lambdas without overhead |
Syntax
[capture-list](params) specifiers exception-spec -> return_type { body }
│ │ │ │ │
│ │ │ │ └─ deduced if omitted
│ │ │ └─ noexcept, noexcept(cond)
│ │ └─ mutable · constexpr (C++17) · consteval (C++20) · static (C++23)
│ └─ optional when no params and no specifiers
└─ required; [] means no capturesCapture forms
| Capture | Meaning |
|---|---|
[] | No captures |
[x] | x by value — copy taken at lambda creation |
[&x] | x by reference |
[=] | All ODR-used locals by value (avoid in member functions — see pitfalls) |
[&] | All ODR-used locals by reference |
[=, &x] | All by value, x by reference |
[&, x] | All by reference, x by value |
[this] | Capture this pointer (C++11) |
[*this] | Copy of *this by value (C++17) |
[x = expr] | Init capture: new closure member initialized from expr (C++14) |
[...xs = std::move(xs)] | Pack expansion in init capture (C++20) |
Examples
Init captures and move-only types (C++14)
Before C++14, a unique_ptr could not enter a lambda. Init captures fix this cleanly:
// C++14
auto ptr = std::make_unique<Widget>(42);
auto task = [p = std::move(ptr)]() { // p is a closure member — moved in
p->run();
};
// ptr is null here; task exclusively owns the WidgetInit captures also compute derived values at capture time rather than call time:
// C++14
auto cb = [limit = config.maxRetries() * 2](int attempt) {
return attempt < limit;
};Generic and template lambdas
C++14 generic lambdas generate a templated operator():
// C++14
auto sum = [](auto first, auto... rest) {
return (first + ... + rest); // fold expression — C++17
};
sum(1, 2, 3); // int
sum(1.0, 2.0f, 3L); // mixed arithmetic typesC++20 template lambdas name the type parameters explicitly, enabling constraints and enforcing that two arguments share the same type — impossible with auto parameters alone:
// C++20
auto max_same = []<typename T>(T a, T b) { return a > b ? a : b; };
max_same(1, 2); // ok — T = int
// max_same(1, 2.0); // error — T deduced to int and double simultaneously
// C++20 — constrained template lambda
auto sum_ints = []<std::integral T>(T a, T b) { return a + b; };Stateless lambdas as comparators (C++20)
Before C++20, closure types were not default-constructible, requiring you to pass an instance to associative containers:
// C++11–C++17
auto cmp = [](int a, int b) { return a < b; };
std::set<int, decltype(cmp)> s(cmp); // must pass instance
// C++20 — captureless lambdas are default-constructible and assignable
std::set<int, decltype([](int a, int b) { return a < b; })> s;
std::priority_queue<Task,
std::vector<Task>,
decltype([](const Task& a, const Task& b) { return a.priority < b.priority; })> pq;Recursive lambdas (C++23)
Before C++23, recursive lambdas required std::function (heap allocation, indirect dispatch) or a Y-combinator. C++23's explicit object parameter enables direct recursion at zero overhead:
// C++23
auto fib = [](this auto& self, int n) -> int {
if (n <= 1) return n;
return self(n - 1) + self(n - 2);
};
fib(10); // 55 — fully inlineable, no heap allocationThe overload pattern (C++17)
Combine multiple lambdas into a single polymorphic callable — idiomatic with std::visit on std::variant:
// C++17
template<typename... Ts>
struct overload : Ts... { using Ts::operator()...; };
template<typename... Ts> overload(Ts...) -> overload<Ts...>; // CTAD guide, C++17
// C++20: deduction guide is implicit — the template is sufficient
std::variant<int, double, std::string> v = 3.14;
std::visit(overload{
[](int x) { std::cout << "int: " << x << '\n'; },
[](double x) { std::cout << "double: " << x << '\n'; },
[](const std::string& s){ std::cout << "string: " << s << '\n'; },
}, v);Pack expansion in init capture (C++20)
Capture a parameter pack by value — previously impossible without a tuple:
// C++20
template<typename... Args>
auto defer_print(Args&&... args) {
return [...captured = std::forward<Args>(args)]() {
(std::cout << ... << captured) << '\n';
};
}
auto f = defer_print(42, " items at ", 3.14, " each");
f(); // 42 items at 3.14 eachBest Practices
Use auto to store lambdas; never std::function unless you need type erasure. std::function implies a heap allocation for larger closures and an indirect call — two costs paid on every invocation. If you need to accept arbitrary callables in a function parameter, prefer a template:
// Slow — heap alloc + indirect call even for simple lambdas
void slow(std::function<bool(int)> pred);
// Zero overhead — concrete type is inlined
template<std::predicate<int> Pred>
void fast(Pred&& pred);Prefer explicit captures over [=] or [&]. Explicit captures document exactly what enters the closure, prevent accidental capture of large objects, and make dangling-reference risks obvious during code review.
Use immediately-invoked lambdas (IIFE) to initialize const values from complex branching logic:
const auto conn = [&]() -> Connection {
if (env == "test") return Connection::in_memory();
return Connection::from_url(config.db_url());
}();In C++20+, never write [=] inside a member function when you also need this. Use [=, this] or [*this] explicitly — [=] implicitly capturing this by pointer is deprecated in C++20.
Add noexcept to lambdas that cannot throw. On hot paths this enables better codegen; for captureless lambdas it propagates to the function-pointer conversion type.
Common Pitfalls
Returning a lambda that captures a local by reference
// UB — local is destroyed before the returned lambda is invoked
auto bad_adder(int n) {
return [&n]() { return n + 1; }; // n dangles
}
// Correct — capture by value
auto good_adder(int n) {
return [n]() { return n + 1; };
}[=] in member functions does not capture members by value
In C++11–C++17, [=] inside a member function captures this by pointer — member accesses inside the lambda still go through that pointer. If the object is destroyed before the lambda runs, you get UB:
struct Timer {
int interval_ = 100;
// C++11–C++17: looks like value capture, but interval_ is read via this
auto wrong() { return [=]() { return interval_; }; }
// ^ UB if Timer is destroyed before the lambda is called
// C++17: true deep copy — safe after Timer's lifetime ends
auto correct() { return [*this]() { return interval_; }; }
};Structured bindings and captures
In C++17, structured binding names are not variables in the language-rule sense and cannot be captured:
auto [x, y] = std::pair{1, 2};
// C++17: ill-formed (Clang error, MSVC warning)
// auto f = [x]() { return x; };
// C++20: structured bindings can be captured directly
auto f = [x]() { return x; }; // okMutable lambda state is per-closure-object, not per-call
auto counter = [n = 0]() mutable { return ++n; };
auto copy = counter; // copy of the closure, with its own n = 0
counter(); // 1
counter(); // 2
copy(); // 1 — copy has its own independent counterGeneralized lambda captures (C++14)
Init captures let the closure own any expression result, not just named locals. The syntax [name = expr] creates a new closure data member named name and initializes it from expr at lambda creation — independent of any existing variable.
// C++14 — move a unique_ptr into the closure
auto ptr = std::make_unique<Widget>(42);
auto task = [p = std::move(ptr)]() { p->run(); };
// ptr is null after this line; task exclusively owns the Widget
// C++14 — compute a derived value once at capture time
auto cmp = [limit = config.maxRetries() * 2](int n) {
return n < limit;
};
// C++14 — rename a captured variable to avoid shadowing
int value = 10;
auto f = [v = value]() { return v * v; }; // closure member is 'v', not 'value'Init captures interact cleanly with mutable: the closure member is non-const by default inside a mutable lambda.
// C++14 — mutable counter seeded from an expression
auto make_counter = [](int start) {
return [n = start]() mutable { return n++; };
};
auto c = make_counter(5);
c(); // 5
c(); // 6Template lambdas (C++20)
C++20 template lambdas expose explicit type parameters, enabling constraints and same-type enforcement that auto parameters cannot express. The template parameter list appears between [] and ().
// C++20 — enforce both arguments share the same type
auto max_same = []<typename T>(T a, T b) -> T {
return a > b ? a : b;
};
max_same(1, 2); // ok — T = int
// max_same(1, 2.0); // error — conflicting deduction for T
// C++20 — constrained template lambda
auto sum_ints = []<std::integral T>(T a, T b) { return a + b; };
sum_ints(3, 4); // ok
// sum_ints(1.5, 2.5); // error — double does not satisfy std::integralTemplate lambdas also give access to the template parameter inside the body — useful when forwarding or working with type-level information:
// C++20 — perfect-forwarding wrapper
auto fwd_call = []<typename F, typename... Args>(F&& f, Args&&... args)
-> decltype(auto)
{
return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
};Immediately invoked lambda expressions (IILE)
An IILE initializes a variable from arbitrarily complex logic while keeping the result const. The lambda is defined and called in the same expression — the () at the end is the invocation.
// C++11 — complex initialization expressed as an IILE
const auto conn = [&]() -> Connection {
if (env == "test") return Connection::in_memory();
if (env == "ci") return Connection::from_env();
return Connection::from_url(cfg.db_url());
}(); // <-- immediately invoked
// Equivalent const variable without IILE requires a non-const temp or a helper functionIILEs are also useful for initializing const members in constructors where a delegating constructor or a helper function would be verbose:
// C++11 — IILE inside member initializer list
struct Config {
const std::vector<int> primes = []() {
std::vector<int> v;
for (int i = 2; i < 50; ++i)
if (is_prime(i)) v.push_back(i);
return v;
}();
};Keep IILEs short. If the body grows beyond a few lines, extract a named helper function instead.
Recursive lambdas
A lambda cannot refer to itself by name during construction, so naive self-recursion requires a workaround. Three patterns cover the common cases, each with different trade-offs.
Pattern 1 — std::function (C++11, simple but has overhead):
// C++11 — recursive lambda via std::function (heap alloc + indirect call)
std::function<int(int)> fib = [&fib](int n) -> int {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
};
fib(10); // 55Pattern 2 — Y-combinator helper (C++14, zero overhead, reusable):
// C++14 — generic Y-combinator
auto Y = [](auto f) {
return [f](auto... args) {
return f(f, args...);
};
};
auto fib = Y([](auto self, int n) -> int {
if (n <= 1) return n;
return self(self, n - 1) + self(self, n - 2);
});
fib(10); // 55Pattern 3 — explicit object parameter (C++23, zero overhead, cleanest syntax):
// C++23 — self-referential lambda with deducing this
auto fib = [](this auto& self, int n) -> int {
if (n <= 1) return n;
return self(n - 1) + self(n - 2); // no double-self trick needed
};
fib(10); // 55 — fully inlineablePrefer pattern 3 in C++23 codebases; prefer the Y-combinator over std::function when C++23 is unavailable and performance matters.
Lambda in unevaluated contexts (C++20)
Before C++20, lambdas were forbidden inside decltype, sizeof, and other unevaluated operands; C++20 lifts this restriction. This unlocks concise type computations and stateless comparator types.
// C++20 — lambda as a comparator type, never instantiated
using CmpType = decltype([](int a, int b) { return a < b; });
std::set<int, CmpType> s; // default-constructed — no instance needed
// C++20 — decltype of a lambda call result
auto transform = [](int x) { return x * x; };
using ResultType = decltype(transform(0)); // int — lambda in unevaluated contextThis feature also enables cleaner requires-clause expressions involving lambdas and works with noexcept specifiers:
// C++20 — noexcept deduced from lambda in unevaluated context
auto safe_op = [](int x) noexcept { return x + 1; };
static_assert(noexcept(safe_op(0))); // ok — lambda is unevaluated hereStateless lambdas and function pointer conversion
A captureless lambda is implicitly convertible to a plain function pointer with a matching signature. This makes them drop-in replacements for C-style callbacks that accept function pointers.
// C++11 — captureless lambda converts to function pointer
auto square = [](int x) { return x * x; };
int (*fp)(int) = square; // implicit conversion — no capture, so it's valid
fp(5); // 25
// Useful with C APIs that take function pointers
std::qsort(arr, n, sizeof(int),
[](const void* a, const void* b) {
return *static_cast<const int*>(a) - *static_cast<const int*>(b);
});The conversion function is constexpr if the body is constexpr (C++17+). Adding any capture — even [=] with nothing actually captured — removes the conversion:
// C++11 — adding a capture breaks function-pointer convertibility
int offset = 0;
auto f = [offset](int x) { return x + offset; };
// int (*fp)(int) = f; // error — f has a closure type with capturesIn C++23, a static lambda is explicitly stateless and also converts to a function pointer, even if written in a context where a capture might otherwise be inferred:
// C++23 — static lambda: explicitly stateless, converts to function pointer
auto logger = [](std::string_view msg) static { std::puts(msg.data()); };
void (*log_fp)(std::string_view) = logger; // okDeducing this in lambdas (C++23)
C++23's explicit object parameter (this auto self) brings full deducing-this support to lambdas, enabling recursive calls, CRTP-style polymorphism, and const/ref qualification forwarding without macros. The first parameter is annotated with this; the compiler deduces its type from how the lambda is invoked.
// C++23 — basic recursive lambda with deducing this
auto sum_tree = [](this auto& self, const Node& n) -> int {
if (!n.left && !n.right) return n.value;
return self(*n.left) + self(*n.right);
};Deducing this also forwards value category, enabling a single lambda body to handle both lvalue and rvalue invocations:
// C++23 — value-category forwarding via deducing this
auto get_data = [data = std::vector<int>{1,2,3}](this auto&& self) -> decltype(auto) {
return std::forward_like<decltype(self)>(self.data);
};
auto& ref = get_data(); // lvalue ref — data is not moved
auto copy = std::move(get_data)(); // rvalue — data is moved outThe explicit object parameter must be the first parameter and cannot be combined with [=]/[&] captures that also implicitly capture this. This feature subsumes the old std::function + [&self] recursive idiom entirely.
See Also
- auto — required for naming closure types without
std::function - STL Algorithms — primary consumers of lambdas as predicates and projections
- Move Semantics — essential for understanding init captures with move-only types
- Fold Expressions — pairs naturally with variadic generic lambdas
- Deducing This — C++23 explicit object parameter; enables recursive lambdas and CRTP without macros