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.
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 expressionstd::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-idget
is looked up in the scope ofE
by class member access lookup, and if that finds at least one declaration, the initializer ise.get<i>()
. Otherwise, the initializer isget<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 entitye
is an lvalue reference and an xvalue otherwise. Given the typeTi
designated bystd::tuple_element<i, E>::type
, eachvi
is a variable of type “reference toTi
” 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 isTi
.