Structured Bindings
C++17 decomposition syntax that unpacks arrays, aggregates, pairs, and tuple-like types into named variables in a single declaration.
Structured Bindingssince C++17A structured binding declaration introduces a set of named identifiers bound to the elements of an array, aggregate, or tuple-like type, replacing manual index access or std::tie unpacking with readable named variables.
Overview
Structured bindings decompose a compound object into named identifiers. The key mental model: auto [a, b] = expr; does not create independent variables. It creates a hidden anonymous object initialized from expr, and a and b are references into that object's elements. This explains all the surprising reference and const behaviors.
Before C++17, unpacking multi-value returns required either std::tie (C++11) into pre-declared variables, or explicit member access like .first/.second. Both are verbose and don't scale beyond two or three elements.
The compiler resolves decomposition by trying three cases in order:
- Array — if the declared type is
T[N], each element binds in declaration order. - Tuple-like protocol — if
std::tuple_size<T>is a complete specialization, the compiler callsget<I>(e)for each index: memberget<I>()first, then ADLget<I>(obj). - Aggregate members — if the type is a class/struct with no user-provided constructor, no private or protected non-static data members, and no virtual functions, members bind in declaration order.
Syntax
auto [a, b] = expr; // anonymous copy of expr; a and b refer into the copy
auto& [a, b] = expr; // a and b refer directly into expr (must be lvalue)
const auto& [a, b] = expr; // const refs into expr; also extends temporary lifetime
auto&& [a, b] = expr; // lvalue ref if expr is lvalue, rvalue ref if expr is rvalueThe cv-qualifiers and ref-qualifier apply to the anonymous entity as a whole — not to individual bindings. auto [const a, b] = expr; is ill-formed; you cannot make only some bindings const.
Examples
Range-for over maps
The most common use case: readable key/value names instead of .first/.second.
#include <map>
#include <string>
#include <print> // C++23
std::map<std::string, int> scores{{"Alice", 95}, {"Bob", 87}, {"Carol", 92}};
for (const auto& [name, score] : scores) { // C++17
std::println("{}: {}", name, score); // C++23
}Multi-value returns
#include <tuple>
#include <string_view>
struct ParseResult { int count; double ratio; std::string_view unit; };
ParseResult parse(std::string_view input);
auto [count, ratio, unit] = parse("42 0.75 seconds"); // C++17
// Accessing getStats().count, getStats().ratio etc. without temporariesinsert / emplace return values
Standard containers returning std::pair<iterator, bool> are a natural fit:
#include <set>
#include <map>
std::set<int> seen;
auto [it, inserted] = seen.insert(42); // C++17
if (!inserted) {
// 42 was already present; *it points to the existing element
}
std::map<std::string, int> cache;
auto [pos, ok] = cache.try_emplace("key", expensive_compute()); // C++17Init-statement in if / switch (C++17)
if (auto [it, ok] = myMap.emplace("session", generate_id()); ok) { // C++17
log_new_session(it->second);
} else {
log_existing_session(it->second);
}Reference bindings and mutation
std::pair<int, int> bounds{0, 100};
auto& [lo, hi] = bounds; // lo and hi refer directly into bounds
lo = 5; // bounds.first is now 5
hi -= 10; // bounds.second is now 90Bit-fields (C++20)
C++20 extended structured bindings to cover bit-field members. Because you cannot form a reference to a bit-field, the bindings are always value copies:
struct Flags {
unsigned ready : 1;
unsigned error : 1;
unsigned code : 6;
};
Flags f{1, 0, 42};
auto [ready, error, code] = f; // C++20; bindings are copies, not refs
// Modifying ready/error/code does NOT affect fCustom tuple protocol
Opt any type into structured bindings without making it an aggregate:
#include <cstdint>
struct Pixel { std::uint8_t r, g, b, a; };
template<> struct std::tuple_size<Pixel>
: std::integral_constant<std::size_t, 4> {};
template<std::size_t I>
struct std::tuple_element<I, Pixel> { using type = std::uint8_t; };
template<std::size_t I>
std::uint8_t get(const Pixel& p) {
if constexpr (I == 0) return p.r; // C++17 if constexpr
else if constexpr (I == 1) return p.g;
else if constexpr (I == 2) return p.b;
else return p.a;
}
Pixel px{255, 128, 0, 255};
auto [r, g, b, a] = px; // r=255, g=128, b=0, a=255The compiler checks for a member get<I>() first; if absent, it falls back to an ADL lookup for a free function get<I>(obj).
Lambda capture (C++20)
Capturing structured bindings in lambda bodies was underspecified in C++17 — some implementations rejected it, others accepted it with subtly wrong semantics. C++20 (P1091) fully standardized this:
auto [x, y] = std::pair{3, 4};
// C++17: capturing x and y is implementation-defined on some compilers
// C++20: fully specified and portable
auto dist_sq = [x, y]() { return x*x + y*y; }; // C++20
// Capture by reference also works C++20+:
auto increment = [&x, &y]() { ++x; ++y; }; // C++20Best Practices
Use const auto& in range-for over containers. auto [k, v] copies each element; for a map<string, ExpensiveType>, that's a string copy and a deep copy per iteration.
for (const auto& [key, val] : myMap) { /* correct */ }
for (auto [key, val] : myMap) { /* copies each pair — rarely intended */ }Use auto& only when you intend to mutate. The ref-qualifier applies to the anonymous entity, so all bindings become references together — there is no mechanism to make only one binding a reference.
Name bindings to match the domain. The payoff of structured bindings is readability. Names like [it, inserted], [lo, hi], [begin, end] communicate intent. Names like [a, b] do not.
Prefer member access for large aggregates when you need only a few fields. Decomposing a ten-member struct to use two of them hides the type and its structure unnecessarily.
Common Pitfalls
Copy vs. reference confusion
std::pair<int, int> p{10, 20};
auto [a, b] = p; // a and b are in an anonymous COPY of p
a = 99; // modifies the copy — p.first is still 10
auto& [c, d] = p; // c and d refer into p directly
c = 99; // p.first is now 99Dangling reference from a temporary
auto& [x, y] = std::pair{1, 2}; // UB: temporary destroyed at end of full expression
// Safe alternatives:
auto [x, y] = std::pair{1, 2}; // copy — always safe
const auto& [x, y] = std::pair{1, 2}; // extends temporary lifetime to binding scopeconst auto& extends the lifetime of the temporary to the scope of the structured binding declaration — the same rule as binding a const lvalue reference to a temporary directly.
Wrong binding count
auto [a, b] = std::tuple{1, 2, 3}; // error: 3 elements, 2 bindings
auto [a, b, c] = std::pair{1, 2}; // error: 2 elements, 3 bindingsThe count must match exactly. There are no defaults, variadics, or wildcard bindings until C++26's placeholder _.
Bit-field bindings are copies, not references
In C++20+ structured bindings over a struct with bit-fields, the bindings are value copies. Mutating a binding has no effect on the original struct member.
Structured bindings are not constexpr before C++26
constexpr auto [a, b] = std::pair{1, 2}; // ill-formed in C++17, 20, 23For constexpr contexts targeting C++17–23, use std::get<0>() and std::get<1>() directly.
Custom Tuple Protocol: Deep Dive
The "tuple-like protocol" lets you opt any class into structured bindings — including types with private members — without making it an aggregate. Three customization points are required:
| What | Where to specialize |
|---|---|
| Element count | std::tuple_size<T> |
Element type at index I | std::tuple_element<I, T> |
Element access at index I | free get<I>(obj) or member get<I>() |
Approach 1: free get function (ADL)
Suitable when you want to add binding support externally, or the type is in a different namespace:
#include <cstdint>
#include <type_traits>
struct Pixel { std::uint8_t r, g, b, a; };
// Step 1 — element count
template<> struct std::tuple_size<Pixel>
: std::integral_constant<std::size_t, 4> {};
// Step 2 — element types
template<std::size_t I>
struct std::tuple_element<I, Pixel> {
static_assert(I < 4);
using type = std::uint8_t;
};
// Step 3 — element access (free function, found via ADL)
template<std::size_t I>
std::uint8_t get(const Pixel& p) noexcept {
if constexpr (I == 0) return p.r;
else if constexpr (I == 1) return p.g;
else if constexpr (I == 2) return p.b;
else return p.a;
}
Pixel px{255, 128, 0, 255};
auto [r, g, b, a] = px; // r=255, g=128, b=0, a=255The compiler looks for a member get<I>() first; if absent, it falls back to ADL for a free get<I>(obj).
Approach 2: member get (deducing this, C++23)
Member get is the cleaner option when the type is under your control and has private fields to expose selectively. C++23's explicit object parameter (this T&&) makes it trivial to write a single overload that handles all value categories:
#include <string>
#include <utility>
class Person {
std::string first_;
std::string last_;
int age_; // intentionally NOT exposed via bindings
public:
Person(std::string f, std::string l, int a)
: first_(std::move(f)), last_(std::move(l)), age_(a) {}
// Deducing-this get<I> covers const/non-const/rvalue in one overload
template<std::size_t I, class Self>
constexpr auto&& get(this Self&& self) noexcept {
if constexpr (I == 0) return std::forward<Self>(self).first_;
else return std::forward<Self>(self).last_;
}
};
template<> struct std::tuple_size<Person>
: std::integral_constant<std::size_t, 2> {};
template<std::size_t I>
struct std::tuple_element<I, Person> {
using type = std::remove_cvref_t<
decltype(std::declval<Person>().template get<I>())>;
};
Person p{"Jane", "Doe", 30};
auto [first, last] = p; // age is not exposedAccessible-members relaxation (C++20, P0969)
In C++17, aggregate-mode structured bindings required all bound members to be public. P0969 (adopted as a DR against C++17, effective in C++20) relaxed this: if the current scope can access the member — through friendship, class-internal declarations, or inheritance — structured bindings can reach it too. Tuple-protocol bindings via get always worked because the function controls access.
C++26: _ as a Placeholder Name (P2169R4)
Before C++26, _ was an ordinary identifier. Reusing it twice in the same scope was a redefinition error, so ignoring multiple elements in nested structured bindings required creativity:
// C++17–23: redefinition error — cannot reuse _ twice in the same scope
for (const auto& [_, v] : myMap) {
const auto& [e, r, _] = v; // error: redefinition of '_'
}C++26 (P2169R4) makes _ a special placeholder name. Any variable, lambda capture, or structured binding element named _ automatically receives an implicit [[maybe_unused]] attribute and may be declared any number of times in the same scope. Mentioning _ in a value expression is ill-formed — it is write-only.
// C++26: _ can appear as many times as needed
for (const auto& [_, v] : myMap) { // C++26 — first _ is fine
const auto& [e, r, _] = v; // C++26 — second _ is also fine
std::println("{}", e);
}
// Discarding unwanted fields from a tuple
auto [x, _, z] = std::tuple{1, 2, 3}; // C++26: middle element discarded
std::println("{} {}", x, z);
// Side-effect-only objects (e.g., locks) no longer need a name
auto _ = std::scoped_lock{mu}; // C++26: no "unused variable" warning
auto _ = std::scoped_lock{mu2}; // second _ in same scope: also fine_ remains a normal identifier when it names a namespace-scope variable (to avoid breaking GMock and similar libraries that use _ as a wildcard). The placeholder semantics only apply to local variables, structured binding elements, and lambda captures.
Compiler support: GCC 14+, Clang 18+.
C++26: constexpr Structured Bindings (P2686R5)
Before C++26, structured bindings could not be declared constexpr, which blocked their use in compile-time computation:
// C++17–23: ill-formed
constexpr auto [a, b] = std::pair{1, 2}; // error before C++26C++26 (P2686R5) lifts this restriction by introducing the concept of symbolic addressing: a constexpr reference no longer requires a globally constant absolute address; it only requires a constant address relative to the enclosing stack frame. This unblocks both constexpr references to local constexpr variables and constexpr structured bindings over tuple-like types:
// C++26
int main() {
constexpr auto [a, b] = std::pair{2, 3};
static_assert(a * b == 6); // OK in C++26
constexpr auto [x, y, z] = std::array{10, 20, 30};
static_assert(x + y + z == 60); // OK in C++26
// Bonus: constexpr reference to local constexpr variable also works
constexpr int n = 42;
constexpr const int& r = n; // was ill-formed before C++26
static_assert(r == 42);
}For constexpr-aware code targeting C++17–23, the workaround remains std::get<I>() directly, or adding static to the bound object to give it a stable address.
Note: as of late 2025 no shipping compiler has implemented P2686R5; expect support in GCC 15+ and Clang 19+.
C++26: Additional Upgrades
Per-element attributes (P0609R3)
C++26 allows attributes directly on individual binding names, not just on the declaration as a whole:
// Before C++26: attribute applies to the whole anonymous entity
[[maybe_unused]] auto [it, inserted] = map.try_emplace(key, val);
// C++26: attribute applies to a specific binding only
auto [it, inserted [[maybe_unused]] ] = map.try_emplace(key, val);Structured binding as a condition (P0963R3)
C++26 allows a structured binding to appear directly as the condition of if, while, or for. The boolean test is applied to the anonymous entity before decomposition:
// C++26
while (auto [header, body] = receive_packet()) {
process(header, body);
}
if (auto [ptr, ec] = std::to_chars(buf, buf + N, value)) {
// conversion succeeded — use ptr
}Binding packs in templates (P1061R10)
C++26 allows a structured binding to introduce a parameter pack inside a template, enabling concise generic code:
// C++26 — only valid inside a template context
template<class P, class Q>
constexpr auto dot_product(const P& p, const Q& q) {
const auto& [...ps] = p; // pack of all elements of p
const auto& [...qs] = q; // pack of all elements of q
static_assert(sizeof...(ps) == sizeof...(qs));
return ((ps * qs) + ...); // fold expression
}
constexpr std::array<int, 3> a{1, 2, 3};
constexpr std::array<int, 3> b{4, 5, 6};
static_assert(dot_product(a, b) == 32); // 1*4 + 2*5 + 3*6Pack structured bindings are limited to template contexts in C++26; non-template pack bindings are expected in a later standard.
See Also
auto— the type deduction rules that underpin structured bindingsstd::tuple— primary use case for the tuple-like protocolstd::pair— simplest tuple-like type; returned by insert/emplacestd::tie— C++11 predecessor for unpacking into pre-existing variables