- Lambda Capture Initializers Reference (cppreference)
- ISO/IEC 14882 C++ Standard Section:
[expr.prim.lambda.capture]
Lambda Capture Initializers (informally termed Init-Captures or Generalized Lambda Captures) allow data members of a synthesized closure class to be declared and initialized directly within a lambda's capture clause using arbitrary expressions ([var = expression] or [&var = expression]). Introduced in C++14 and extended in C++20 with variadic parameter pack expansions ([...args = std::move(args)]), Init-Captures resolve a major limitation of C++11 lambdas: they enable moving move-only objects (std::unique_ptr, std::thread, std::packaged_task) into closures, allow creating self-contained stateful members without outer-scope dummy variables, and execute expensive computations once at closure construction time.
In C++11, lambda capture clauses supported only two basic syntax forms: capture-by-value ([var]) and capture-by-reference ([&var]).
// C++11 Move-Only Resource Capture Failure
auto ptr = std::make_unique<int>(42);
// auto bad_closure = [ptr]() { ... };
// COMPILE ERROR in C++11! Capture-by-value attempts to COPY std::unique_ptr!- Inability to Transfer Resource Ownership: Move-only types (such as
std::unique_ptr,std::thread,std::future, or custom move-only sockets) could not be captured by value into a lambda because C++11 capture syntax attempted to invoke the copy constructor (= delete). - Outer Scope Pollution for Local State: Storing private, stateful variables inside a C++11 closure required declaring dummy variables in the outer scope solely for the lambda to capture:
// C++11 Workaround: Pollutes outer scope with 'dummy_counter' int dummy_counter = 0; auto logger = [dummy_counter](int val) mutable { return ++dummy_counter; };
- Inability to Rename Captured Symbols: C++11 provided no syntax to rename captured variables or capture the result of expressions.
Solution: C++14 introduced Generalized Lambda Captures ([var = expr]), evaluating expr during closure object construction and initializing a new private data member inside the synthesized closure class using auto type deduction rules.
When the compiler parses an Init-Capture lambda:
auto ptr = std::make_unique<int>(100);
// C++14 Init-Capture moving 'ptr' into closure member 'resource' and creating 'counter'
auto processor = [resource = std::move(ptr), counter = 0](int delta) mutable {
*resource += delta;
return ++counter;
};It generates an anonymous closure class where resource and counter are mapped directly to private non-static data members:
// Equivalent Compiler-Generated Synthesized Closure Class
class __SynthesizedInitCaptureClosure_C14 {
private:
std::unique_ptr<int> resource; // Initialized via std::move(ptr)
int counter; // Initialized to 0
public:
__SynthesizedInitCaptureClosure_C14(std::unique_ptr<int> r, int c)
: resource(std::move(r)), counter(c) {}
// 'mutable' keyword removes 'const' qualification on operator()
int operator()(int delta) {
*resource += delta;
return ++counter;
}
};- Value Init-Capture (
[var = expression]): Deducesvaras if declaredauto var = expression;. The expression is evaluated once when the lambda is instantiated, NOT when the lambda's call operator (operator()) is invoked. - Reference Init-Capture (
[&var = expression]): Deducesvaras an lvalue reference (auto& var = expression;), bindingvardirectly toexpression.
┌───────────────────────────────────────────────────────────────────┐
│ C++14 │
├───────────────────────────────────────────────────────────────────┤
│ • Introduced Generalized Lambda Captures / Init-Captures │
│ • Enabled moving move-only objects into closures ([p = move(ptr)])│
│ • Introduced isolated closure data members ([counter = 0]) │
└───────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ C++17 │
├───────────────────────────────────────────────────────────────────┤
│ • Combined Init-Captures with [*this] value copy captures │
│ • Evaluated Init-Capture expressions in constexpr contexts │
└───────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ C++20 │
├───────────────────────────────────────────────────────────────────┤
│ • Variadic Pack Init-Captures (P0780R2): │
│ [...args = std::move(args)]() { ... } │
│ • Class Template Argument Deduction (CTAD) in Init-Captures │
└───────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ C++23 / C++26 │
├───────────────────────────────────────────────────────────────────┤
│ • Structured Binding Captures in Lambdas (C++26) │
└───────────────────────────────────────────────────────────────────┘
Demonstrates capturing move-only smart pointers into asynchronous task closures without deep copies or outer reference dependencies.
#include <iostream>
#include <memory>
#include <thread>
#include <string>
struct DatabaseConnection {
std::string endpoint;
explicit DatabaseConnection(std::string ep) : endpoint(std::move(ep)) {}
void execute_query(const std::string& query) const {
std::cout << "Executing '" << query << "' on " << endpoint << "\n";
}
};
int main() {
auto db_conn = std::make_unique<DatabaseConnection>("db.production.internal:5432");
// C++14 Init-Capture: Moves 'db_conn' into closure member 'conn'
auto async_task = [conn = std::move(db_conn), query = std::string("SELECT * FROM users")]() {
conn->execute_query(query);
};
// 'db_conn' is now nullptr in caller scope!
std::cout << "Original db_conn pointer is null: " << std::boolalpha << (db_conn == nullptr) << "\n";
// Launch worker thread moving closure into thread context
std::thread worker(std::move(async_task));
worker.join();
return 0;
}Demonstrates creating internal closure state and evaluating initialization expressions at closure instantiation time.
#include <iostream>
#include <vector>
#include <numeric>
int main() {
std::vector<int> dataset = {10, 20, 30, 40, 50};
// 1. Evaluates std::accumulate ONCE during closure construction!
// 2. Declares private member 'total_sum' inside closure without outer scope variables.
auto average_calculator = [total_sum = std::accumulate(dataset.begin(), dataset.end(), 0),
count = dataset.size()]() {
return static_cast<double>(total_sum) / count;
};
std::cout << "Computed Average: " << average_calculator() << " (Expected: 30.0)\n";
return 0;
}Demonstrates perfect forwarding and moving variadic parameter packs into lambdas using C++20 pack Init-Captures.
#include <iostream>
#include <utility>
template <typename... Args>
auto make_deferred_logger(Args&&... args) {
// C++20 Pack Init-Capture: Moves/forwards each element in parameter pack 'args'!
return [...captured_args = std::forward<Args>(args)]() {
auto print_elem = [](const auto& val) { std::cout << val << " "; };
(print_elem(captured_args), ...); // Fold expression over expanded captured pack
std::cout << "\n";
};
}
int main() {
auto logger = make_deferred_logger(101, "Telemetry", 3.14159, 'A');
std::cout << "Executing Deferred Pack Logger:\n";
logger(); // Prints: 101 Telemetry 3.14159 A
return 0;
}-
Init-Capture Expressions Execute ONCE During Lambda Construction:
[val = compute_expensive()]callscompute_expensive()when the lambda object is constructed, not when the lambda is called (logger()).
-
Always Use
mutableWhen Mutating Primitive Init-Captures:- By default, the synthesized
operator()of a closure class isconst. Attempting to modify an integer member initialized via[counter = 0]will trigger a compile-time error unless the lambda is declaredmutable:// Correct Mutating Init-Capture auto step = [counter = 0]() mutable { return ++counter; };
- By default, the synthesized
-
Avoid Dangling Reference Init-Captures (
[&ref = expr]):- Be extremely careful when using reference Init-Captures (
[&var = local_var]). If the closure outliveslocal_var, calling the lambda results in a Dangling Reference Use-After-Free crash.
- Be extremely careful when using reference Init-Captures (
- Related to:
- Lambda Expressions — Baseline C++11 closure syntax.
- Generic Lambda Expressions —
autoparameters in closures. - Lambda Capture this By Value — C++17
[*this]object copy capture. - Move Semantics & Special Member Functions — Underpins
std::movetransfers in Init-Captures.
- Superseded by: None.
- Supersedes: Outer-scope dummy variable declarations used for closure state initialization.
Back to C++14 section.