C++ Structured Bindings: Capture by Value vs Reference

This article explores the mechanisms and differences between capturing structured bindings by value versus by reference inside lambda expressions in modern C++. Introduced in C++17, structured bindings decompose objects into constituent identifiers, but capturing them in lambdas was only standardized in C++20. Understanding whether to capture these bindings by value or by reference is critical for managing object lifetimes, avoiding undefined behavior, controlling memory overhead, and determining whether modifications affect the original decomposed object.

The Underlying Nature of Structured Bindings

To understand capture mechanics, you must first understand how structured bindings work under the hood. When you write:

auto [a, b] = get_pair();

The compiler creates an invisible, unnamed temporary object that holds the returned value, and a and b act as aliases to the subobjects of that hidden instance. If you write auto& [a, b] = my_pair;, a and b refer directly to the members of my_pair.

Because structured bindings are not traditional variables, C++17 did not allow them to be captured by lambdas. C++20 introduced explicit support for capturing structured bindings, handling value and reference semantics differently based on the underlying hidden object.

Capturing by Value

When capturing a structured binding by value, such as [a] or via default capture [=], the compiler copies the subobject referenced by the binding into the lambda’s closure class.

  • Independence: The captured entity inside the lambda is a completely separate copy. Mutating it (if the lambda is marked mutable) will not affect the original object or the hidden object created during decomposition.
  • Lifetime Safety: Because the value is copied into the lambda closure, the lambda can safely outlive the scope in which the structured binding was introduced. Returning the lambda or dispatching it asynchronously to another thread does not cause dangling reference issues.
  • Performance Considerations: For large or non-trivially copyable types, capturing by value introduces copy overhead. If the bound entity is expensive to duplicate, capturing by value can lead to performance degradation.
struct Point { int x; int y; };

void example_value() {
    Point pt{10, 20};
    auto [x, y] = pt;

    auto lambda = [x]() mutable {
        x += 5; // Modifies the lambda's internal copy only
        return x;
    };

    lambda(); // Returns 15
    // pt.x is still 10, and the local binding 'x' is still 10
}

Capturing by Reference

Capturing a structured binding by reference, such as [&a] or via default capture [&], binds a reference member inside the closure directly to the subobject designated by the binding.

  • Direct Mutation: Any modifications made through the captured reference directly alter the corresponding member of the decomposed object. If the structured binding was formed using auto [x, y], the reference points to the hidden compiler-generated object. If it was formed using auto& [x, y], the reference points directly to the original source object.
  • Lifetime Risks: If the structured binding was initialized by value (auto [x, y] = get_temp();), the hidden object has automatic storage duration tied to the local block. If a lambda capturing x or y by reference outlives that enclosing block, invoking the lambda results in undefined behavior due to dangling references.
  • Efficiency: Capturing by reference avoids copying data, making it the preferred approach for large types, heavy containers, or types that have deleted copy constructors, provided the lambda does not outlive the bound variables.
void example_reference() {
    Point pt{10, 20};
    auto& [x, y] = pt;

    auto lambda = [&x]() {
        x += 5; // Directly modifies pt.x
    };

    lambda();
    // pt.x is now 15
}

Key Differences Summary

  • Storage: Value capture allocates storage inside the closure and invokes the copy constructor; reference capture stores a reference or pointer.
  • Side Effects: Value capture isolates state inside the closure; reference capture reflects mutations back to the referenced subobject.
  • Lifetimes: Value capture allows the lambda to safely escape the local scope; reference capture requires the underlying decomposed object to remain alive for the entire lifespan of the lambda.