Structured Bindings: When Something Looks Like a Reference and Behaves Similarly to a Reference, But It's Not a Reference

structured bindings: when something looks like a reference and behaves similarly to a reference, but it's not a reference

I wrote this yesterday:

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.

Type of reference binding from std::tuple

It's true that the type of variable r0 is int&. But v0, here called a, is a different thing, the name of a structured binding, and is not technically a variable at all, just a different sort of name.

So we need to look at the description of decltype, in [dcl.type.decltype]:

For an expression e, the type denoted by decltype(e) is defined as follows:

  • if e is an unparenthesized id-expression naming a structured binding, decltype(e) is the referenced type as given in the specification of the structured binding declaration;

So here the "referenced type" is just int.

Of course, decltype((a)) is int& as we'd expect. decltype on a name without doubled parentheses is used to find out how a name is "declared", not how the name behaves, so it's not obvious that decltype(a) "should" be one thing or another, since a doesn't have a normal declaration. Though it is potentially a bit useful that given std::tuple<int, int&> t{0, n}; auto& [a, b] = t;, we have decltype(a) is int but decltype(b) is int&.

Is decltype(auto) for a structured binding supposed to be a reference?

The identifers themselves are references. From [dcl.struct.bind]/3:

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.

That is, a and b are both int&&.

But the way decltype(auto) actually behaves comes from [dcl.type.auto.deduct]:

If the placeholder is the decltype(auto) type-specifier, T shall be the placeholder alone. The type deduced for T is determined as described in [dcl.type.simple], as though e had been the operand of the decltype.

This wording is really awkward, but ultimately:

decltype(auto) e = a;
~~~~~~~~~~~~~~

means:

decltype( a  ) e = a;
~~~~

and decltype(a) means, from [dcl.type.simple]/4.1:

if e is an unparenthesized id-expression naming a structured binding ([dcl.struct.bind]), decltype(e) is the referenced type as given in the specification of the structured binding declaration;

The referenced type of a is int, so e must be an int. Which means it's not a reference, and clang is correct. Filed 81176.

Range-based for loop on unordered_map and references

Structured bindings are modeled as aliases, not "real" references. Even though they may use a reference under the hood.

Imagine that you have

struct X {
const int first = 0;
int second;
int third : 8;
};

X x;
X& y = x;

What's decltype(x.second)? int. What's decltype(y.second)? int. And so in

auto& [first, second, third] = x;

decltype(second) is int, because second is an alias for x.second. And third poses no problems even though it's not allowed to bind a reference to a bit-field, because it's an alias, not an actual reference.

The tuple-like case is designed to be consistent with that. Even though in that case the language has to use references, it does its best to pretend that those references do not exist.

What are the types of identifiers introduced by structured bindings in C++17?

if I print out std::is_reference<decltype(a)>::value, I get 0 in the first case 1 in the second.

Why is that even if we can prove that a and b refer to the elements in the tuple and one can modify those values by means of them?

I'm not a language lawyer, but probably it is due to this bullet of the standard (working draft):

if e is an unparenthesized id-expression naming a structured binding [...], decltype(e) is the referenced type as given in the specification of the structured binding declaration


Side note. You should use this form to do so that a and b refer to the elements in the tuple:

auto tup = std::make_tuple(1, 2);
auto & [ a, b ] = tup;

It follows a minimal, working example:

#include <tuple>
#include <type_traits>
#include <iostream>

int main() {
auto tup = std::make_tuple(1, 2);
auto & [ a, b ] = tup;
a = 0;
std::cout << a << ", " << std::get<0>(tup) << std::endl;
}

See it on Coliru. On the other side, what you get is a copy of the values using the expression below:

auto [ a, b ] = std::make_tuple(1, 2);

Here is an article that explains it better and is a bit more comprehensible than the standard for humans.

Does copy elision work with structured bindings

Does mandatory copy elision apply to decomposition via structured bindings? Which of the following cases does that apply to?

Yes, all of them. The point of structured bindings is to give you named references to the destructured elements of the type you're binding to. This:

auto [one, two] = expr;

Is just syntax sugar for:

auto __tmp = expr;
some_type<0,E>& a = some_getter<0>(__tmp);
some_type<1,E>& b = some_getter<1>(__tmp);

Where some_type and some_getter depend on the kind of type we're destructuring (array, tuple-like, or type with all public non-static data members).

Mandatory copy elision applies in the auto __tmp = expr line, none of the other lines involve copies.


There's some confusion around an example in the comments, so let me elaborate on what happens in:

auto [one, two] = std::make_tuple(Something{}, Something{});

That expands into:

auto __tmp = std::make_tuple(Something{}, Something{}); // note that it is from
// std::make_tuple() itself that we get the two default constructor calls as well
// as the two copies.
using __E = std::remove_reference_t<decltype(__tmp)>; // std::tuple<Something, Something>

Then, since __E is not an array type but is tuple-like, we introduce variables via an unqualified call to get looked up in the associated namespace of __E. The initializer will be an xvalue and the types will be rvalue references:

std::tuple_element_t<0, __E>&& one = get<0>(std::move(__tmp));
std::tuple_element_t<1, __E>&& two = get<1>(std::move(__tmp));

Note that while one and two are both rvalue references into __tmp, decltype(one) and decltype(two) will both yield Something and not Something&&.

Introduced intermediate variable in structured binding definition?

The intent is to disallow redeclaring structured bindings as references. See CWG 2313.

Type of variables in structured binding

decltype(e) behaves differently depending on what e is given as the argument. For the structured binding, decltype yields what follows, [dcl.type.simple]:

For an expression e, the type denoted by decltype(e) is defined as follows:

  • if e is an unparenthesized id-expression naming a structured binding, decltype(e) is the referenced type as given in the specification of the structured binding declaration

The referenced type for a structured binding declaration with an array type expression as the initializer is the type of the element [dcl.struct.bind]:

If E is an array type with element type T, the number of elements in the identifier-list shall be equal to the number of elements of E. Each vi is the name of an lvalue that refers to the element i of the array and whose type is T; the referenced type is T. [ Note: The top-level cv-qualifiers of T are cv. — end note ]

cv-qualifier propagation in structured binding

decltype has a special rule when the member of a class is named directly as an unparenthesized member access expression. Instead of producing the result it would usually if the expression was treated as an expression, it will result in the declared type of the member.

So decltype(rf.x) gives int, because x is declared as int. You can force decltype to behave as it would for other expressions by putting extra parentheses (decltype((rf.x))), in which case it will give const int& since it is an lvalue expression and an access through a const reference.

Similarly there are special rules for decltype if a structured binding is named directly (without parentheses), which is why you don't get const int& for decltype(x).

However the rules for structured bindings take the type from the member access expression as an expression if the member is not a reference type, which is why const is propagated. At least that is the case since the post-C++20 resolution of CWG issue 2312 which intends to make the const propagation work correctly with mutable members.

Before the resolution the type of the structured binding was actually specified to just be the declared type of the member with the cv-qualifiers of the structured binding declaration added, as you are quoting in your question.

I might be missing some detail on what declared type refers to exactly, but it seems to me that this didn't actually specify x to have type const int& in your first snippet (and decltype hence also not const), although that seems to be how all compilers always handled that case and is also the only behavior that makes sense. Maybe it was another defect, silently or unintentionally fixed by CWG 2312.

So, practically speaking, both rf.x and x in your example are const int lvalue expressions when you use them as expressions. The only oddity here is in how decltype behaves.



Related Topics



Leave a reply



Submit