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:
A&
&
becomesA&
A&
&&
becomesA&
A&&
&
becomesA&
A&&
&&
becomesA&&
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.
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:
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 thatT
will be deduced asType&
, whereType
is the type of the lvalue. Obviously if the lvalue isconst
, it will be deduced asconst Type&
. The reference collapsing rules mean thatType & &&
becomesType &
forv
, an lvalue reference. Which is exactly what we need to callCall
. 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: aType
temporary expression or certainType&&
expressions), thenT
will be deduced asType
. The reference collapsing rules give usType &&
, 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 getType&
as the type ofv
. This will bind to a non-const lvalue reference. If we call it with a const lvalue, we getconst Type&
, which will only bind to a const lvalue reference argument inCall
.If you call
Fwd
with an xvalue, we again getType&&
as the type ofv
. 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 ifCall
used aconst&
, we could callFwd
with an xvalue.If you call
Fwd
with a prvalue, we again getType&&
, 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 getType&
. This will not bind to an rvalue reference parameter, so a compile error results. Aconst Type&
also won't bind to an rvalue reference parameter, so it still fails. And this is exactly what would happen if we calledCall
directly with an lvalue.If you call
Fwd
with an xvalue, we getType&&
, 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 (returningType&
).