Do structured bindings and forwarding references mix well?

  • A+
Category:Languages

I know I can do

auto&& bla = something(); 

and depending on the constness of the return value of something, I'd get a different type for bla.

Does this also work in the structured bindings case, e.g.

auto&& [bla, blabla] = something(); 

I would guess so (structured bindings piggy-back on auto initializers, which behave like this), but I can't find a definitive yes.

Update: Preliminary tests seem to do what I expect (derive the constness properly):

#include <tuple>  using thing = std::tuple<char, int*, short&, const double, const float&>;  int main() {     char c = 0;     int i = 1;     short s = 2;     double d = 3.;     float f = 4.f;      thing t{c, &i, s, d, f};      auto&& [cc, ii, ss, dd, ff] = t;      c = 10;     *ii = 11;     ss = 12;     dd = 13.;     ff = 14.f; } 

Live demo, gives error as I'd expect if auto&& is doing its job:

main.cpp: In function 'int main()': main.cpp:20:10: error: assignment of read-only reference 'dd'      dd = 13.;           ^~~ main.cpp:21:10: error: assignment of read-only reference 'ff'      ff = 14.f; 

I'd still like to know exactly where this behaviour is specified.

Note: Using "forwarding references" to mean this behaviour might be stretching it, but I don't have a good name to give the const deduction part of auto&& (or template-T&& for that matter).


Yes. Structured bindings and forwarding references mix well.

In general, any place you can use auto, you can use auto&& to acquire the different meaning. For structured bindings specifically, this comes from [dcl.struct.bind]:

Otherwise, e is defined as-if by

attribute-specifier-seqopt decl-specifier-seq ref-qualifieropt e initializer ;

where the declaration is never interpreted as a function declaration and the parts of the declaration other than the declarator-id are taken from the corresponding structured binding declaration.

There are further restrictions on these sections in [dcl.dcl]:

A simple-declaration with an identifier-list is called a structured binding declaration ([dcl.struct.bind]). The decl-specifier-seq shall contain only the type-specifier auto and cv-qualifiers. The initializer shall be of the form “= assignment-expression”, of the form “{ assignment-expression }”, or of the form “( assignment-expression )”, where the assignment-expression is of array or non-union class type.

Putting it together, we can break down your example:

auto&& [bla, blabla] = something(); 

as declaring this unnamed variable:

auto               && e = something(); ~~~~               ~~     ~~~~~~~~~~~ decl-specifier-seq        initializer                    ref-qualifier 

The behavior is that is derived from [dcl.spec.auto] (specifically here). There, we do do deduction against the initializer:

template <typename U> void f(U&& ); f(something()); 

where the auto was replaced by U, and the && carries over. Here's our forwarding reference. If deduction fails (which it could only if something() was void), our declaration is ill-formed. If it succeeds, we grab the deduced U and treat our declaration as if it were:

U&& e = something(); 

Which makes e an lvalue or rvalue reference, that is const qualified for not, based on the value category and type of something().

The rest of the structured bindings rules follow in [dcl.struct.bind], based on the underlying type of e, whether or not something() is an lvalue, and whether or not e is an lvalue reference.


With one caveat. For a structured binding, decltype(e) always is the referenced type, not the type you might expect it be. For instance:

template <typename F, typename Tuple> void apply1(F&& f, Tuple&& tuple) {     auto&& [a] = std::forward<Tuple>(tuple);     std::forward<F>(f)(std::forward<decltype(a)>(a)); }  void foo(int&&);  std::tuple<int> t(42); apply1(foo, t); // this works! 

I pass my tuple is an lvalue, which you'd expect to pass its underlying elements in as lvalue references, but they actually get forwarded. This is because decltype(a) is just int (the referenced type), and not int& (the meaningful way in which a behaves). Something to keep in mind.

There are two places I can think of where this is not the case.

In trailing-return-type declarations, you must use just auto. You can't write, e.g.:

auto&& foo() -> decltype(...); 

The only other place I can think of where this might not be the case is part of the Concepts TS where you can use auto in more places to deduce/constrain types. There, using a forwarding reference when the type you're deducing isn't a reference type would be ill-formed I think:

std::vector<int> foo(); std::vector<auto> a = foo();   // ok, a is a vector<int> std::vector<auto&&> b = foo(); // error, int doesn't match auto&& 

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: