70

The following link provides the 4 forms of reference collapsing (if I'm correct that these are the only 4 forms): http://thbecker.net/articles/rvalue_references/section_08.html.

From the link:

  1. A& & becomes A&
  2. A& && becomes A&
  3. A&& & becomes A&
  4. A&& && becomes A&&

Although I can make an educated guess, I would like a concise explanation for the rationale behind each of these reference-collapsing rules.

A related question, if I might: Are these reference-collapsing rules utilized in C++11 internally by such STL utilities such as std::move(), std::forward(), and the like, in typical real-world use cases? (Note: I'm specifically asking whether the reference-collapsing rules are utilized in C++11, as opposed to C++03 or earlier.)

I ask this related question because I am aware of such C++11 utilities as std::remove_reference, but I do not know if the reference-related utilities such as std::remove_reference are routinely used in C++11 to avoid need for the reference-collapsing rules, or whether they are used in conjunction with the reference-collapsing rules.

Dan Nissenbaum
  • 13,558
  • 21
  • 105
  • 181
  • 4
    You'd probably be interested in [this video](http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Scott-Meyers-Universal-References-in-Cpp11) by Scott Meyers. – Benjamin Lindley Dec 05 '12 at 14:50
  • Looking at it now. Thanks! – Dan Nissenbaum Dec 05 '12 at 14:53
  • 1
    *"typical real-world use cases"* - Perfect forwarding (which I would call a very useful real-world use case) builds entirely on those reference collapsing rules (and might in turn have been the practical reason for designing those rules like they are, I think). By introducing a new type of references for utilizing move semantics they also got the chance to define the corresponding collapsing rules to facilitate perfect forwarding and thus solve two problems with a single feature. – Christian Rau Dec 05 '12 at 15:49
  • 1
    I think `A& & becomes A&` is **not** reference collapsing. – Nawaz Sep 24 '15 at 12:06
  • 1
    [Youtube link to Scott Meyers video](https://www.youtube.com/watch?v=6lq8PntQMV4) the top comment msdn link is dead unfortunately. Title: "C++ and Beyond 2012: Scott Meyers - Universal References in C++11" – arkan Sep 25 '22 at 01:59

3 Answers3

53

The reference collapsing rules (save for A& & -> A&, which is C++98/03) exist for one reason: to allow perfect forwarding to work.

"Perfect" forwarding means to effectively forward parameters as if the user had called the function directly (minus elision, which is broken by forwarding). There are three kinds of values the user could pass: lvalues, xvalues, and prvalues, and there are three ways that the receiving location can take a value: by value, by (possibly const) lvalue reference, and by (possibly const) rvalue reference.

Consider this function:

template<class T>
void Fwd(T &&v) { Call(std::forward<T>(v)); }

By value

If Call takes its parameter by value, then a copy/move must happen into that parameter. Which one depends on what the incoming value is. If the incoming value is an lvalue, then it must copy the lvalue. If the incoming value is an rvalue (which collectively are xvalues and prvalues), then it must move from it.

If you call Fwd with an lvalue, C++'s type-deduction rules mean that T will be deduced as Type&, where Type is the type of the lvalue. Obviously if the lvalue is const, it will be deduced as const Type&. The reference collapsing rules mean that Type & && becomes Type & for v, an lvalue reference. Which is exactly what we need to call Call. Calling it with an lvalue reference will force a copy, exactly as if we had called it directly.

If you call Fwd with an rvalue (ie: a Type temporary expression or certain Type&& expressions), then T will be deduced as Type. The reference collapsing rules give us Type &&, which provokes a move/copy, which is almost exactly as if we had called it directly (minus elision).

By lvalue reference

If Call takes its value by lvalue reference, then it should only be callable when the user uses lvalue parameters. If it's a const-lvalue reference, then it can be callable by anything (lvalue, xvalue, prvalue).

If you call Fwd with an lvalue, we again get Type& as the type of v. This will bind to a non-const lvalue reference. If we call it with a const lvalue, we get const Type&, which will only bind to a const lvalue reference argument in Call.

If you call Fwd with an xvalue, we again get Type&& as the type of v. This will not allow you to call a function that takes a non-const lvalue, as an xvalue cannot bind to a non-const lvalue reference. It can bind to a const lvalue reference, so if Call used a const&, we could call Fwd with an xvalue.

If you call Fwd with a prvalue, we again get Type&&, so everything works as before. You cannot pass a temporary to a function that takes a non-const lvalue, so our forwarding function will likewise choke in the attempt to do so.

By rvalue reference

If Call takes its value by rvalue reference, then it should only be callable when the user uses xvalue or rvalue parameters.

If you call Fwd with an lvalue, we get Type&. This will not bind to an rvalue reference parameter, so a compile error results. A const Type& also won't bind to an rvalue reference parameter, so it still fails. And this is exactly what would happen if we called Call directly with an lvalue.

If you call Fwd with an xvalue, we get Type&&, which works (cv-qualification still matters of course).

The same goes for using a prvalue.

std::forward

std::forward itself uses reference collapsing rules in a similar way, so as to pass incoming rvalue references as xvalues (function return values that are Type&& are xvalues) and incoming lvalue references as lvalues (returning Type&).

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 1
    Excellent answer. I am hung up on this: `If you call Fwd with an lvalue, C++'s type-deduction rules mean that T will be deduced as Type&`. Perhaps my lack of understanding is beyond the scope of a comment, and I need to study this more (or post another question). But, I do not understand why type deduction would choose `T` as `Type&` rather than `Type`. – Dan Nissenbaum Dec 05 '12 at 15:59
  • @DanNissenbaum: Several reasons, but the simplest is the most obvious: it won't copy the value (which may not actually be copyable). – Nicol Bolas Dec 05 '12 at 16:34
  • If `T` is deduced as `Type`, than the *argument type* to `Fwd()` would be `Type&&`, though, right? I guess that's what I don't understand. – Dan Nissenbaum Dec 05 '12 at 16:49
  • @DanNissenbaum: And then the forwarding wouldn't be *perfect*. If I pass an lvalue to `Fwd`, `Fwd` needs to pass an lvalue to `Call`. `Fwd` forwards a `Type&&` as an *xvalue*, not an lvalue. Thus, the forwarding isn't perfect. Also, that rule is part of C++98, where rvalue references don't exist. – Nicol Bolas Dec 05 '12 at 17:09
  • 2
    As I've thought about it, it is clear that the compiler could *not* deduce `T` to be `Type` if the argument to a call to `Fwd` is an lvalue, because the function signature would then be `Fwd(Type &&)`, and the compiler would not allow an lvalue to be passed to a function accepting an rvalue reference. The next possible choice is that `T` is `Type &`, which corresponds to a function signature `Fwd(Type & &&)`, which (according to reference collapsing rules) becomes `Fwd(Type &)`, which is an acceptable function signature, so it is used. – Dan Nissenbaum Dec 05 '12 at 18:35
  • 1
    In the first paragraph of the section "By rvalue reference", I'm pretty sure it should say prvalue instead of lvalue. – acdx Sep 04 '17 at 19:24
  • 2
    (Reposted with a minor fix): This part is incorrect: "If you call `Fwd` with an xvalue (ie: certain `Type&&` expressions), then `T` will be deduced as `Type&&.`" Instead, `T` is deduced [as a non-reference](https://godbolt.org/g/KjhoNy). The parameter type *is* `Type &&` and no reference collapsing is necessary. The same is true for prvalues, i.e. any rvalue. – Arne Vogel Jan 10 '18 at 12:42
10

The rules are actually pretty simple. Rvalue reference is a reference to some temporary value that does not persist beyond the expression that uses it - in contrast to lvalue reference which references persisting data. So if you have a reference to a persisting data, no matter what other references you combine it with, the actual referenced data is an lvalue - this covers for the first 3 rules. The 4th rule is natural as well - rvalue reference to rvalue reference is still a reference to non-persistent data, hence rvalue reference is yielded.

Yes, the C++11 utilities rely on these rules, implementation provided by your link matches the real headers: http://en.cppreference.com/w/cpp/utility/forward

And yes, the collapsing rules along with template argument deduction rule are being applied when using std::move and std::forward utilities, just like explained in your link.

The usage of type traits such as remove_reference is really depends on your needs; move and forward cover for the most casual cases.

SomeWittyUsername
  • 18,025
  • 3
  • 42
  • 85
  • 2
    In case 3, though, you have `A&& &`, so you "start off" with a reference to a *temporary* value (`A&&`), and then take a reference to it (`A&& &`), so wouldn't this **not** be in the category '`if you have a reference to a persisting data, no matter what other references you combine it with...`' (since you don't start with a reference to persisting data)? – Dan Nissenbaum Dec 05 '12 at 17:59
  • 3
    @DanNissenbaum this is a good observation. I suppose that despite the starting point as an rvalue reference the compiler inspects the entire expression to make a more educated decision. The relationship between & and && is like a logical `and` between `false`(&) and `true`(&&). – SomeWittyUsername Dec 05 '12 at 18:06
  • 2
    "lvalue" is an expression category, it is not possible to describe "the actual referenced data" as an lvalue. Also rvalue references may refer to non-temporary objects (e.g. `X&& y = std::move(x);`) – M.M Sep 04 '17 at 21:16
1

A noob in C++ here. I am just trying to share my collective understanding so far. The rationale behind the reference collapsing rules had been piquing my mind for quite some time, but I feel some what resolved now based on the following thought train.

This is a perspective twist on SomeWittyUsername's answer. My thought train for the rationale behind the reference collapsing rules is presented as a progressively elaborating bullets list.

  • References (lvalue and rvalue) can be considered as object handle aliases
  • Rvalue references also mark an object's property that it can be moved from
  • In case of reference to reference, the first reference handle can be looked at as the handle with which an object is being handed off, and the second one as the handle with which it is being received as a parameter
  • The first reference in reference to reference can be looked at as the one that tells us what is allowed on the object
    • This is the referenceness that is contributed by the template type parameter
  • The second reference in reference to reference can be looked at as the one that tells us what the parameter intends to do with it
    • This is the referenceness that is contributed by the template function parameter
  • In combinations where either movability is not allowed on the original object (T& &&) or the parameter doesn’t intend to move from it (T&& &), or both (T& &), the compiler treats it as non movable from for that particular usage
  • Only in the combination where the original object is movable from and the parameter also intends to move from it (T&& &&), the compiler treats it as a movable from object for that particular usage
Dhwani Katagade
  • 939
  • 11
  • 22