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 usingauto& [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 capturingxoryby 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.