Skip to content

Latest commit

 

History

History
243 lines (188 loc) · 12.2 KB

lambda-capture-initializers.md

File metadata and controls

243 lines (188 loc) · 12.2 KB

Lambda Capture Initializers (Init-Capture)


Executive Abstract

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.


Motivation & Historical Context

Pre-C++14 Deficiencies: The Move-Only Resource Barrier

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!

Deficiencies of C++11 Lambda Captures:

  1. 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).
  2. 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; };
  3. 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.


Technical Mechanics & Synthesized Class Member Mapping

1. Under the Hood: Synthesized Closure Class Transformation

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;
    }
};

2. Value Categories & Lifetime Mechanics

  • Value Init-Capture ([var = expression]): Deduces var as if declared auto 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]): Deduces var as an lvalue reference (auto& var = expression;), binding var directly to expression.

Evolution Across Standards (C++14 to C++26/C++29)

┌───────────────────────────────────────────────────────────────────┐
│ 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)                  │
└───────────────────────────────────────────────────────────────────┘

Comprehensive Code Examples

Example 1: Moving Move-Only Types (std::unique_ptr) into Thread Tasks

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;
}

Example 2: Self-Contained Closure State & One-Time Pre-computation

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;
}

Example 3: Modern C++20 Variadic Pack Init-Captures ([...args = std::move(args)])

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;
}

Engineering Best Practices & Common Pitfalls

  1. Init-Capture Expressions Execute ONCE During Lambda Construction:

    • [val = compute_expensive()] calls compute_expensive() when the lambda object is constructed, not when the lambda is called (logger()).
  2. Always Use mutable When Mutating Primitive Init-Captures:

    • By default, the synthesized operator() of a closure class is const. Attempting to modify an integer member initialized via [counter = 0] will trigger a compile-time error unless the lambda is declared mutable:
      // Correct Mutating Init-Capture
      auto step = [counter = 0]() mutable { return ++counter; };
  3. Avoid Dangling Reference Init-Captures ([&ref = expr]):

    • Be extremely careful when using reference Init-Captures ([&var = local_var]). If the closure outlives local_var, calling the lambda results in a Dangling Reference Use-After-Free crash.

Relationships


Back to C++14 section.