Ch25.2: Structured Bindings
1. Unpacking Without the Ceremony
A structured binding declares several names at once and binds them to the elements of an object. It is the feature that turns this:
auto result = try_insert(map, key, value);
auto it = result.first;
bool inserted = result.second;
into this:
auto [it, inserted] = try_insert(map, key, value);
The syntax is a declaration with a bracketed identifier list:
auto [a, b] = pair;
auto &[x, y] = pair;
auto const &[x, y] = pair;
auto &&[x, y] = make_pair();
The declaration is not a tuple type and the names are not separate variables in the usual sense. The compiler introduces one hidden object, initializes it from the right-hand side, and then makes each name refer to one element of that hidden object.
2. What Can Be Decomposed
| Kind | Elements | Example |
|---|---|---|
| Array | The array elements, in order | int a[3]; auto [x, y, z] = a; |
| Tuple-like |
std::tuple, std::pair, std::array, or anything
with tuple_size, tuple_element and get
|
auto [x, y] = std::pair{1, 2}; |
| Aggregate | The public non-static data members, in declaration order | struct point { int x, y; }; auto [x, y] = point{1, 2}; |
The number of names must match the number of elements. A mismatch is a compile error. There
is no "skip one" syntax; use a placeholder name such as _ if you only need some
of the values.
struct point
{
int x;
int y;
};
point p{3, 4};
auto [x, y] = p; // x == 3, y == 4
auto &[rx, ry] = p; // rx and ry refer to p.x and p.y
rx = 10; // p.x is now 10
For an aggregate, only public direct non-static data members participate. A class with private members, virtual functions or a user-declared constructor is not decomposed as an aggregate. Base classes are not a flat list of members; do not rely on a particular order when inheritance is involved.
3. auto, auto&, auto&&
The qualifier on the declaration applies to the hidden object, not to each name separately. This is the part that causes lifetime and mutation bugs.
| Declaration | Hidden object | Names |
|---|---|---|
auto [a, b] = expr; |
A copy of expr |
References into the copy; modifying a does not modify expr |
auto &[a, b] = expr; |
A reference to expr |
References into expr; modifying a modifies expr |
auto const &[a, b] = expr; |
A const reference | Read-only; extends the lifetime of a temporary |
auto &&[a, b] = expr; |
A forwarding reference |
Extends a temporary's lifetime like auto const &, but preserves
mutability and value category
|
std::pair<int, int> make();
auto [a, b] = make(); // copies the pair
auto &[c, d] = make(); // error: cannot bind a non-const lvalue reference to a temporary
auto const &[e, f] = make(); // OK: lifetime extended
auto &&[g, h] = make(); // OK: rvalue reference to the temporary
The last two look similar but are not. auto const & extends the temporary's
lifetime to the end of the scope. auto && binds an rvalue reference; the
temporary still lives to the end of the full expression unless lifetime extension applies.
If you need the values later, use auto or auto const &.
4. The Map Case
The most common real use is iterating a map and inserting into one:
std::map<std::string, int> counts;
for (auto const &[key, value] : counts)
{
std::print("{} -> {}\n", key, value);
}
if (auto [it, inserted] = counts.try_emplace("apple", 1); inserted)
{
std::print("new key: {}\n", it->first);
}
In a map, key is const: the container owns the ordering and will not let you
mutate the key through an iterator. value is mutable when the binding is
non-const. This is not a quirk of structured bindings; it is the map's iterator type showing
through.
The if init-statement keeps it and inserted in scope
for the condition and the body. That is the idiomatic way to use the result of
try_emplace without leaking the variables into the rest of the function.
5. Tuple-Like Types and Customization
For a tuple-like type, the compiler uses std::tuple_size,
std::tuple_element and get. The standard types already provide
them. A user-defined type can opt in:
struct version
{
int major;
int minor;
};
namespace std
{
template <>
struct tuple_size<version> : integral_constant<std::size_t, 2>
{
};
template <std::size_t I>
struct tuple_element<I, version>
{
using type = int;
};
}
template <std::size_t I>
int get(version const &v)
{
if constexpr (I == 0)
{
return v.major;
}
else
{
return v.minor;
}
}
version v{1, 2};
auto [major, minor] = v;
This is a lot of machinery for a two-field struct. The aggregate path already handles public data members. Use the tuple protocol when the type is not an aggregate, or when the decomposition order is not the declaration order.
The get function is found by argument-dependent lookup. It must be visible at
the point of the structured binding. A member get is also considered in some
contexts, but a free function in the type's namespace is the portable form.
6. Subtleties That Bite
-
You cannot apply
const,volatileorstaticto one name. The qualifier applies to the hidden object; useauto const &or copy the value into a separate variable. - Structured bindings are not variables you can take the address of as a group. Each name refers to a subobject of the hidden object. Taking the address of a name is allowed when it is an lvalue, but there is no address for the binding as a whole.
-
A bit-field cannot be bound to a non-const reference. A copy is fine; an
auto &binding is not. -
Lifetime is the hidden object's lifetime. A binding that refers into a
temporary needs the temporary to live;
auto const &is the usual fix. -
constexprstructured bindings are C++26. Older standards rejectconstexpr auto [x, y] = ...even though the initializer is constant. - Lambda captures are per name. Capturing the individual names is clearer than trying to capture the binding as a unit, and it avoids lifetime surprises.
7. A Small Example: Parsing a Result
struct parse_result
{
bool ok;
int value;
};
parse_result parse(std::string_view text)
{
if (text == "42")
{
return {true, 42};
}
return {false, 0};
}
int main()
{
if (auto [ok, value] = parse("42"); ok)
{
std::print("value = {}\n", value);
}
else
{
std::print("parse failed\n");
}
}
The init-statement is not part of the structured binding; it is the ordinary
if init-statement from Ch3.3. The two
features compose naturally: the binding declares the names, the init-statement scopes them.
Key Takeaways
- Structured bindings unpack arrays, tuple-like types and public aggregate members.
- The declaration creates one hidden object; the names refer into it.
autocopies,auto &aliases,auto const &extends a temporary,auto &&forwards.- The number of names must match the number of elements.
- Map keys are const; values are mutable.
- Tuple-like customization is available, but aggregates usually need nothing.
- Watch lifetime, bit-fields and the C++26-only
constexprform.
The next page moves from unpacking values to the declarations that decide where values live: storage duration, linkage and namespaces.