Structured bindings unpack a pair, a tuple, an array, or the public members of a struct into individually named variables in a single declaration. Before C++17, getting at the pieces of a std::pair meant typing .first and .second everywhere, and iterating a std::map meant writing it->first and it->second. This chapter covers the syntax, the three categories of types the standard supports, the reference-binding modes, the common cases like map iteration and multi-return functions, the rules for hooking custom types into the feature, and the limits.
A structured binding declaration looks like a normal auto declaration, except the variable name is replaced by a bracketed list of names.
auto [name, price] = product; introduces two variables, name and price, by unpacking product. The compiler figures out the types from the right-hand side: name is a std::string and price is a double. The number of names in the brackets has to match the number of pieces the right-hand side exposes, two in this case.
The leading auto is not optional. Options include auto, auto&, const auto&, or auto&&, but it cannot be replaced with an explicit type list. The names inside the brackets are not their own types; the compiler deduces those.
Structured bindings are a pure language feature. There is no runtime overhead. The compiler rewrites the declaration into something equivalent to a hidden anonymous object plus a set of named references into it, all resolved at compile time.
Structured bindings work on exactly three kinds of types, and the compiler picks which rule to apply based on the right-hand side. Knowing which case applies helps when something does not compile, because the error messages are not always friendly.
If the right-hand side is a built-in array of N elements, the brackets must contain exactly N names. Each name binds to the corresponding array element.
With plain auto, the array is copied element by element into a hidden array, and the names bind to elements of the copy. With auto&, the names bind to the original array's elements directly. This is the only built-in case where structured bindings work; std::array works, but it goes through case 2, not case 1.
std::tuple_size and getIf the right-hand side is a type for which std::tuple_size<T>::value is defined, the compiler treats it as a tuple-like type. The number of names must match std::tuple_size<T>::value, and each name [i] is initialized from get<i>(rhs) (looked up via argument-dependent lookup or as a member, in that order).
std::pair, std::tuple, and std::array all opt into this protocol, which is why the same syntax works across all three.
The compiler rewrites the binding to roughly this:
The __tmp part lives as long as the binding does, so item, quantity, and unitPrice keep referring to valid memory. __tmp is not visible directly; it is an implementation detail of the language.
std::array goes through the same machinery, with std::tuple_size<std::array<T, N>>::value == N.
If the right-hand side is a class or struct that has no tuple_size specialization, the compiler falls back to its public data members in declaration order. The number of names has to match the number of public non-static data members.
A few rules go with this case. All non-static data members must be public; if any member is private or protected, the binding is ill-formed. The members must all be in the same class, so members inherited from a base class are not visible to the binding (the standard tightened this in C++17 itself). The order is the declaration order in the source, not alphabetical, not by access specifier.
This case is appropriate for user-defined types where a clean destructuring API is wanted.
Plain auto [a, b, c] = obj; makes a full copy of obj into a hidden temporary. For a struct with three ints, that is nothing. For a struct holding a std::string and a std::vector, the copy is real. Use const auto& or auto& to avoid the copy.
The qualifier on auto controls how the hidden object on the right-hand side is captured. The names inside the brackets are always references to subobjects of that hidden object, but whether that hidden object is a copy or a reference is determined by the qualifier.
auto [a, b] = rhs; makes a copyThe hidden object is a copy of rhs. The names refer to subobjects of the copy. Modifying them does not affect rhs.
auto& [a, b] = rhs; binds to the originalThe hidden object is a reference to rhs. The names refer to subobjects of rhs itself. Modifying them modifies the original.
This is the form for iterating a container and updating values in place.
const auto& [a, b] = rhs; reads without copyingThe hidden object is a const reference to rhs. The names are read-only views into rhs. No copy happens. This is the default for read-only access to large objects.
auto&& [a, b] = rhs; is a forwarding referenceA forwarding reference (sometimes called a universal reference) binds to anything: lvalues, rvalues, const, non-const. It behaves like auto& when rhs is an lvalue and like a real rvalue reference when rhs is an rvalue. This is the safe default in generic code, especially in range-based for loops over containers whose element type is unknown.
For std::map, auto& and auto&& behave the same way; both bind to the live element in the map. For ranges that return proxy objects (some views from <ranges>, for example), auto&& is the one that always works.
A side-by-side summary of when each mode makes sense:
| Mode | Copy made? | Mutates source? | When to use |
|---|---|---|---|
auto [a, b] = x | Yes | No | Small types, when an independent copy is wanted |
auto& [a, b] = x | No | Yes | Iterating and modifying in place |
const auto& [a, b] = x | No | No | Read-only access to large objects |
auto&& [a, b] = x | No | Depends | Generic code, ranges, third-party types |
Map iteration is the case where structured bindings improve daily code the most. Before C++17, the canonical loop looked like this:
With C++11's range-based for, it improved a little, but pair<const Key, Value> still appeared explicitly:
C++17 allows writing what the code means:
The element type of std::map<int, std::string> is std::pair<const int, std::string>, which the binding splits into its two halves. Even with auto&, the key half is still const, because the map's value_type is a pair whose first member is const. The compiler honors that automatically; trying to assign to orderId would not compile.
Updating values in place is just as clean:
The first loop uses auto& so the assignment modifies the map. The second loop uses const auto& because it only reads. This pair of loops is a common shape in code: walk the map once to mutate, walk it again to report.
std::unordered_map works the same way. The binding does not care about the underlying container, only that the element type is a pair.
const auto& [k, v] on a map iteration is zero overhead. auto [k, v] copies both key and value on every iteration, which can be expensive for string keys or large values. Default to references unless a copy is needed.
The diagram traces what happens during map iteration with a structured binding. The container yields pairs whose first half is const. The binding splits the pair into two named references. k inherits the const qualifier from the pair's first type, so the key stays read-only even when the outer binding is auto&. v is a regular non-const reference and can be assigned to.
Functions that need to return more than one value have always been awkward in C++. The classic options are an out-parameter, a std::pair, a std::tuple, or a custom struct. Each had real downsides at the call site. Structured bindings remove the call-site pain for the last three, which makes "return a tuple" the default style in modern C++ for short multi-return signatures.
std::pairstd::pair is the simplest case: two values, accessed at the call site by name.
The function returns std::pair<bool, std::string> constructed from a braced list. The caller unpacks it into ok and error with meaningful names. The intent reads cleanly at the call site, which is the point.
std::tupleThree or more values argue for std::tuple. The auto-deduced return type plus a braced return list keeps the function body readable.
The downside of returning a tuple is that the order is positional. Swapping two members at the call site is something the compiler cannot always catch. For long-lived APIs, a named struct is the more maintainable choice.
When the return type has more than two or three components, or when it is part of a public API, a struct with named fields is more readable than tuple. Structured bindings still work on it through case 3.
The function's signature documents the meaning of each returned value through the struct field names. The caller can destructure if convenient, or access the named fields directly (result.orderId) when only one is needed. Either way works.
std::tie for pre-C++17 assignmentsA related pattern from C++11 appears in older code: std::tie. It builds a tuple of references and assigns a pair or tuple into already-declared variables.
The new structured-binding form is cleaner: auto [ok, err] = validate();. std::tie is still useful for one narrow case the new syntax does not cover, namely assigning into existing variables instead of declaring new ones. Otherwise, prefer the new form.
Most of the time, case 3 (public data members) is enough for user types. Sometimes structured-binding support is wanted on a type whose internals are private, or a different view than the literal field order. The way to do that is to opt into the tuple-like protocol.
Opting in means specializing three things in namespace std:
std::tuple_size<YourType> to declare how many pieces the type exposes.std::tuple_element<I, YourType> to declare the type of each piece.get<I> function (either a free function found by ADL, or a member template <std::size_t I> get()) to return each piece.A Cart class that hides its members but allows destructuring as auto [items, total]:
A few things are worth unpacking. tuple_size<Cart> declares that a Cart has two pieces. tuple_element<I, Cart> declares each piece's type. The member get<I> returns the actual data. The if constexpr ladder is the cleanest way to pick the right member at compile time; each branch returns a different type, and only the matching one is instantiated.
Specializations in namespace std are one of the few cases where the standard explicitly allows additions to the standard namespace, provided the specialization is for a user-defined type. Specializing for someone else's type is not allowed.
If a type's data is public and the declaration order matches what should be exposed, skip the protocol entirely and let case 3 handle it. The protocol is for cases where case 3 does not fit: private fields, derived classes, computed views, or types like boost::variant where the "pieces" are not literal data members.
The custom protocol is a compile-time hookup, not a runtime cost. Each get<I>() is inlined by any modern optimizer, so a binding into a customized type costs the same as accessing the underlying members directly.
The diagram walks the decision tree the compiler runs when it sees a structured binding. First it asks whether the right-hand side is a built-in array (case 1). If not, it looks for a tuple_size specialization (case 2). Failing that, it tries the public-members rule (case 3). If none of those apply, the binding is ill-formed. Knowing which branch the code is hitting helps when a structured binding refuses to compile, because the error tends to come from inside whichever branch the compiler picked.
Structured bindings cover most cases that come up, but a handful of corners do not work or behave subtly. The list below is the one to keep handy.
Every element of the tuple, array, or struct must be named. There is no placeholder like _ in the language, even though some style guides reserve _ as a convention for "ignore this".
If _ already exists in the scope, redeclaring it in the binding shadows it without warning. The current best workaround is to name the unused variable explicitly and ignore it. C++26 is expected to add a real wildcard, but it is not here yet.
constexpr directly until C++26C++17 through C++23 do not allow constexpr auto [a, b] = ... at all. The binding itself is not a constant expression. The bound names can be read in a constexpr context if the right-hand side and the underlying type allow it, but the declaration itself cannot carry the constexpr keyword.
C++26 adds this feature, but most production codebases will be on C++17 or C++20 for a while, so plan around it.
In C++17, capturing a structured-binding name in a lambda is technically not allowed (the binding name is not considered a "variable" in the strictest reading of the standard). Compilers were inconsistent about this. C++20 fixed the wording, and modern g++/clang in C++20 mode accept it.
For C++17 targets, copy the binding into a regular variable before capturing it.
The number of names must exactly match the number of pieces. Binding only the first two elements of a five-element tuple and ignoring the rest is not allowed.
A single auto& qualifier applies to the whole binding. One name cannot be a reference and another a copy in the same declaration.
For that pattern, declare two separate variables the traditional way.
Case 3 allows binding to bit-field members, but the binding has to be by value or by auto&&. A bit-field cannot be bound to a regular reference, because a reference must have a real address and bit-fields do not have one.
This corner is rare in real code, but it does come up in interop with packed structures.
None of these limitations have runtime cost; they are compile-time restrictions. If the code compiles, the binding is as efficient as the equivalent direct access.
Combining several pieces from this chapter, a small program that walks a cart, finds the most expensive item, and reports the result through a named struct:
Three bindings show up in eight lines of logic. The outer loop binds each map entry into name and line. A nested binding splits line into quantity and unitPrice. At the call site, the function's struct return is unpacked into three named variables. Each binding picks the mode that fits: const auto& in the loop (read-only over a map), const auto& for the nested pair (read-only), and plain auto for the return value (own copy wanted). The body reads like a description of what the program does, not like the mechanics of getting at struct members.
10 quizzes