2

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.

yeputons
  • 8,478
  • 34
  • 67

1 Answers1

2

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.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • While this makes sence, doesn't the quoted text state that `vi` must be a reference (to something in this underlying `e`, whenever it is a reference or not)? – Lapshin Dmitry Apr 21 '20 at 11:06
  • @LapshinDmitry - It does. Like I mentioned, CPP insights does not correctly represent that. – StoryTeller - Unslander Monica Apr 21 '20 at 11:17
  • If bindings are references into an underlying object, how can they be bound to a temporary object? They should be lvalue-references because `__f17` is a lvalue. – yeputons Apr 21 '20 at 11:50
  • 1
    The lvalue-ness of `__f17 ` has little to do with this. The initializer for the binding is `__f17.get<0>()`, that's an rvalue, so the reference type is an rvalue reference (clearly stated in the text). As far as binding references to temporaries, are you at all familiar with [reference lifetime extension](https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary)? That's how. – StoryTeller - Unslander Monica Apr 21 '20 at 11:55
  • @StoryTeller-UnslanderMonica Ok, thanks! Yet I stil ldon't get something: I have built a different sample that returns references from `get` so I should get references (and it works from asserts I have placed), yet decltype reports I don't have those: https://godbolt.org/z/4ekbud – Lapshin Dmitry Apr 21 '20 at 12:11
  • @LapshinDmitry - It's a quirk of `decltype` https://stackoverflow.com/questions/60830650/structural-binding-and-type-of-variables – StoryTeller - Unslander Monica Apr 21 '20 at 12:12
  • @StoryTeller-UnslanderMonica Oh wow, thanks. Why would it do it, though? Pretend it's actually a "tuple element" in the first place? – Lapshin Dmitry Apr 21 '20 at 12:19
  • @LapshinDmitry - I can only speculate. My guess is that it's to make structured bindings feel more like regular variables instead of references in disguise, since their declarations look more or less like regular objects. – StoryTeller - Unslander Monica Apr 21 '20 at 12:22
  • Ah, I see. I was thinking that "the initializer" in "where the reference is an lvalue reference if the initializer is an lvalue and an rvalue reference otherwise" refers to the initializer of `e` somewhy. – yeputons Apr 21 '20 at 14:18