What are the reference collapsing rules, and how are they utilized by the C++ standard library?

c++, language-lawyer, reference, reference-collapsing, stl

Solution

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

Problem

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&` `&` becomes `A&` - `A&` `&&` becomes `A&` - `A&&` `&` becomes `A&` - `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.

Original source