Search code examples
c++structured-bindings

Why does structured binding introduce variables as values, not references?


I'm learning about structured binding declarations. My understanding was that in auto& [x, y] = expr; variables x and y are introduced of types "reference to std::tuple_element<i, E>::type" (for i=0, 1 and E is the type of the invisible variable e). Moreover, these variables are initialized with get<i>(e).

So, if I use auto& and get<> returns a value (not a reference), it should not compile, as you cannot bind an lvalue to a temporary. However, the following example builds for me in some versions of GCC, Clang, and Visual Studio:

#include <cstddef>
#include <tuple>
#include <type_traits>

struct Foo {
    template<std::size_t i>
    int get() { return 123; }
};

namespace std {
    template<> struct tuple_size<Foo> : integral_constant<size_t, 1> {};
    template<std::size_t i> struct tuple_element<i, Foo> { using type = int; };
}

int main() {
    Foo f;
    auto& [x] = f;
    x++;
}

Moreover, C++ Insights clearly shows that clang expands the structured binding to:

Foo f = Foo();
Foo & __f17 = f;
std::tuple_element<0, Foo>::type x = __f17.get<0>();
x++;

Here, it declares x not as a reference, but as a value. Why is that?

I expected lvalue references and compilation error: e (__f17 in the example above) is an lvalue reference.


Solution

  • That is because auto& does not apply to the structured bindings. It is applied to the underlying entity that refers to the structure. In your cppinsights snippet, that would be __f17.

    If you were to use auto [x] instead, the snippet would expand to something like this

    Foo f = Foo();
    Foo __f17 = f; // Difference here
    std::tuple_element<0, Foo>::type x = __f17.get<0>();
    x++;
    

    The bindings themselves are always a sort of reference into an underlying object. The cppinsights code doesn't accurately represent that however. The relevant passages in the C++ standard say this

    [dcl.struct.bind]

    3 Otherwise, if the qualified-id std​::​tuple_­size<E> names a complete type, the expression std​::​tuple_­size<E>​::​value shall be a well-formed integral constant expression and the number of elements in the identifier-list shall be equal to the value of that expression. The unqualified-id get is looked up in the scope of E by class member access lookup, and if that finds at least one declaration, the initializer is e.get<i>(). Otherwise, the initializer is get<i>(e), where get is looked up in the associated namespaces. In either case, get<i> is interpreted as a template-id. [ Note: Ordinary unqualified lookup is not performed.  — end note ] In either case, e is an lvalue if the type of the entity e is an lvalue reference and an xvalue otherwise. Given the type Ti designated by std​::​tuple_­element<i, E>​::​type, each vi is a variable of type “reference to Ti” initialized with the initializer, where the reference is an lvalue reference if the initializer is an lvalue and an rvalue reference otherwise; the referenced type is Ti.