15

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).

Community
  • 1
  • 1
rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • Since there are three cases to consider (based on `something`'s return type, which you happened to omit). Detailing all of them, like this question requires, makes this more than a little broad. – StoryTeller - Unslander Monica Apr 09 '18 at 08:44
  • if it has a name, it's not an r-value reference AFAIR. – Richard Hodges Apr 09 '18 at 08:44
  • @RichardHodges never said anything about rvalue references. I'm more concerned about the const vs non-const lvalue reference case. – rubenvb Apr 09 '18 at 08:57
  • "Universal references" (now known as "forwarding references") appear only in templates. These are rvalue references. Completely different things. It's confusing that they both are denoted by && in different contexts. – Jive Dadson Apr 09 '18 at 09:06
  • Have you tried it? - To answer your question, sure, why not? – Jive Dadson Apr 09 '18 at 09:06
  • @RichardHodges the *reference* has a name. – Quentin Apr 09 '18 at 09:11
  • @Jive answering a question asking for the specification of behaviour by inverting the question is somewhat unhelpful. See update for my attempt to verify it. This still leaves me asking if this is indeed the intended and specified behaviour according to the C++ Standard. I've also added a note explaining my usage of "univeral references" in this context. I don't think this subset of their behaviour has ever been given a separate name. – rubenvb Apr 09 '18 at 09:19
  • FWIW behaviour is as I’d expect too, consistent with deduced types of singular auto&& variables. I do hope it’s specified by the standard. – Richard Hodges Apr 09 '18 at 10:02

1 Answers1

8

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&&
Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Regarding your caveat on `decltype`: you could argue the reverse, i.e. that without the current spec it would be surprising for [this static assertion to fire](http://coliru.stacked-crooked.com/a/9126cfed9b6c464a). (The argument doesn’t hold for arrays however.) – Luc Danton Apr 09 '18 at 16:28
  • @LucDanton That strikes me as begging the question? Why would you assume that should hold, without already knowing that it holds? – Barry Apr 09 '18 at 16:47
  • The same way I assume `&tup.val == &a` to hold, i.e. two different ways of ultimately referring to the same thing. – Luc Danton Apr 09 '18 at 17:06
  • @LucDanton More to the point, there's no way to differentiate between `auto&& [a] = tup; auto&& [b] = std::move(tup);` since `decltype` gives the same type for `a` and `b`. – Barry Apr 09 '18 at 17:07
  • @LucDanton `&tup.val == &a` does not to me imply that `decltype(a)` must be `int`. – Barry Apr 09 '18 at 17:08
  • that's not really the argument here; in any case you can construct more examples with `std::same_type_v` given an identity function and so on and so forth – Luc Danton Apr 09 '18 at 17:09
  • @LucDanton But you still haven't addressed *why* you think that *should* be the case? As opposed to `a` being an `int&`? (and `b` being an `int&&`) – Barry Apr 09 '18 at 17:12