Structured binding declaration (since C++17)

< cpp‎ | language

Binds the specified names to subobjects or elements of the initializer.

attr(optional) cv-auto ref-operator(optional) [ identifier-list ] = expression ; (1) (since C++17)
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] { expression } ; (2) (since C++17)
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] ( expression ) ; (3) (since C++17)
attr - sequence of any number of attributes
cv-auto - possibly cv-qualified type specifier auto
ref-operator - either & or &&
identifier-list - list of comma-separated identifiers (structured bindings) introduced by this declaration
expression - an expression that does not have the comma operator at the top level (grammatically, an assignment-expression), and has either array or non-union class type

A structured binding declaration introduces all identifiers in the identifier-list as names in the surrounding scope and binds them to subobjects or elements of the object denoted by expression. The names so introduced are called structured bindings.

A structured binding declaration first introduces a uniquely-named variable (here denoted by e) to hold the value of the initializer, as follows:

  • If expression has array type A and no ref-operator is present, then e has type cv A, where cv is the cv-qualifiers in the cv-auto sequence, and each element of e is copy- (for (1)) or direct- (for (2,3)) initialized from the corresponding element of expression.
  • Otherwise e is defined as if by using its name instead of [ identifier-list ] in the declaration.

We use E to denote the type of the expression e. (In other words, E is the equivalent of std::remove_reference_t<decltype((e))>.)

A structured binding declaration then performs the binding in one of three possible ways, depending on E:

  • Case 1: if E is an array type, then the names are bound to the array elements.
  • Case 2: if E is a non-union class type and std::tuple_size<E> is a complete type, then the "tuple-like" binding protocol is used.
  • Case 3: if E is a non-union class type but std::tuple_size<E> is not a complete type, then the names are bound to the public data members of E.

Each of the three cases is described in more detail below.

Each structured binding has a referenced type, defined in the description below. This type is the type returned by decltype when applied to an unparenthesized structured binding.


[edit] Case 1: binding an array

Each identifier in the identifier-list becomes the name of an lvalue that refers to the corresponding element of the array. The number of identifiers must equal the number of array elements.

The referenced type for each identifier is the array element type. Note that if the array type E is cv-qualified, so is its element type.

int a[2] = {1,2};
auto [x,y] = a; // creates e[2], copies a into e, then x refers to e[0], y refers to e[1]
auto& [xr, yr] = a; // xr refers to a[0], yr refers to a[1]

[edit] Case 2: binding a tuple-like type

The expression std::tuple_size<E>::value must be a well-formed integer constant expression, and the number of identifiers must equal std::tuple_size<E>::value.

Each identifier becomes a variable whose type is "reference to std::tuple_element<i, E>::type": lvalue reference if its corresponding initializer is an lvalue, rvalue reference otherwise. The initializer for the i-th identifier is

  • e.get<i>(), if lookup for the identifier get in the scope of E by class member access lookup finds at least one declaration (of whatever kind)
  • Otherwise, get<i>(e), where get is looked up by argument-dependent lookup only, ignoring non-ADL lookup.

In these initializer expressions, e is an lvalue if the type of the entity e is an lvalue reference and an xvalue otherwise (this effectively performs a kind of perfect forwarding), and <i> is always interpreted as a template parameter list.

The referenced type for the i-th identifier is std::tuple_element<i, E>::type.

float x{};
char  y{};
int   z{};
const auto& [a,b,c] = std::tuple<float&,char&&,int>(x,std::move(y),z);
// a is a float& that refers to x
// b is a char& that refers to y
// but decltype(b) is char&&
// c is a const int& that refers to the 3rd element of the tuple
// but decltype(c) is const int
// This structured binding declaration is roughly equivalent to
const auto& e = std::tuple<float&,char&&,int>(x,std::move(y),z);
using E = std::remove_reference_t<decltype((e))>;//E is const std::tuple<float&,char&&,int>
float& a = get<0>(e); // float& is the result of std::tuple_element<0,E>::type&
char& b = get<1>(e); // char& is the result of std::tuple_element<1,E>::type&
// however, decltype(b) is std::tuple_element<1,E>::type, i.e. char&&
const int& c = get<2>(e); // const int& is the result of std::tuple_element<2,E>::type&
// however, decltype(c) is std::tuple_element<2,E>::type, i.e. const int

[edit] Case 3: binding to public data members

Every non-static data member of E must be a public direct member of E or the same unambiguous public base of E, and E may not have any anonymous union member. The number of identifiers must equal the number of non-static data members.

Each identifier in identifier-list becomes an lvalue that refers to the next member of e in declaration order (bit fields are supported); the type of the lvalue is cv T_i, where cv is the cv-qualifiers of E and T_i is the declared type of the i-th member.

The referenced type of the i-th identifier is cv T_i.

struct S {
    int x1 : 2;
    volatile double y1;
S f();
const auto [x, y] = f(); // x is a const int lvalue identifying the 2-bit bit field
                         // y is a const volatile double lvalue

[edit] Notes

The lookup for get in the tuple-like case is similar to the lookup for begin and end in the range-based for loop. In particular, any member named get, even a type or an enumerator, will cause the member interpretation to be used, even if the member is not accessible.

The portion of the declaration preceding [ applies to the hidden variable e, not to the introduced identifiers.

int a = 1, b = 2;
const auto& [x, y] = std::tie(a, b); // x and y are of type int&
auto [z, w] = std::tie(a, b);        // z and w are still of type int&
assert(&z == &a);                    // passes

The tuple-like interpretation is always used if std::tuple_size<E> is a complete type, even if that would cause the program to be ill-formed:

struct A { int x; };
namespace std {
    template<> struct tuple_size<::A> {};
auto [x] = A{}; // error; the "public data member" interpretation is not considered.

The usual rules for reference-binding to temporaries (including lifetime-extension) apply if a ref-operator is present and the expression is a prvalue. In those cases the hidden variable e is a reference that binds to the temporary variable materialized from the prvalue expression, extending its lifetime. As usual, the binding will fail if e is a non-const lvalue reference:

int a = 1;
const auto& [x] = std::make_tuple(a); // OK, not dangling
auto&       [y] = std::make_tuple(a); // error, cannot bind auto& to rvalue std::tuple
auto&&      [z] = std::make_tuple(a); // also OK

decltype(x), where x is a structured binding, names the referenced type of that structured binding. In the tuple-like case, this is the type returned by std::tuple_element, which may not be a reference even though the structured binding itself is in fact always a reference in this case. This effectively emulates the behavior of binding to a struct whose non-static data members have the types returned by tuple_element, with the referenceness of the binding itself being a mere implementation detail.

std::tuple<int, int&> f();
auto [x, y] = f();       // decltype(x) is int
                         // decltype(y) is int&
const auto [z, w] = f(); // decltype(z) is const int
                         // decltype(w) is int&

[edit] Example

#include <set>
#include <string>
#include <iomanip>
#include <iostream>
int main() {
    std::set<std::string> myset;
    if (auto [iter, success] = myset.insert("Hello"); success) 
        std::cout << "insert is successful. The value is " << std::quoted(*iter) << '\n';
        std::cout << "The value " << std::quoted(*iter) << " already exists in the set\n";


insert is successful. The value is "Hello"

[edit] See also

creates a tuple of lvalue references or unpacks a tuple into individual objects
(function template) [edit]