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

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

The next page moves from unpacking values to the declarations that decide where values live: storage duration, linkage and namespaces.