Reference Collapsing

Concise explanation of reference collapsing rules requested: (1) A& & - A& , (2) A& && - A& , (3) A&& & - A& , and (4) A&& && - A&&

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

Reference collapsing rules not applying as expected?

Actually, no referencing collapsing occurs. The relevant function template to pay attention to, i.e., the one selected, is:

template<class T>
T&& fwd(T& t) { // <-- not a forwarding reference
return static_cast<T&&>(t);
}

Note that this function template has no forwarding references – the function parameter, t, is just an lvalue reference (T& t).

The T template parameter is deduced to int – not int& – because t is not a forwarding reference but just an lvalue reference. If you simply replace T by int in the function template above, then you will obtain:

template<class T>
int&& fwd(int& t) {
return static_cast<int&&>(t);
}

No reference collapsing is applied as there is no such a thing here that would otherwise end up becoming a reference to a reference (e.g., int& && or int&& &&).

Why reference collapsing rules work only for templates?

So why does line 1 works?

There is a special rule in template argument deduction that was introduced to permit perfect-forwarding. In the context of template argument deduction, T&& is not an rvalue reference but a forwarding reference instead.

If an lvalue is passed to a function template taking a forwarding reference, the type parameter is deduced as T& instead of T. This allows reference collapsing to take place: T& && becomes T&.

From cppreference:

If P is an rvalue reference to a cv-unqualified template parameter (so-called forwarding reference), and the corresponding function call argument is an lvalue, the type lvalue reference to A is used in place of A for deduction (Note: this is the basis for the action of std::forward Note: in class template argument deduction, template parameter of a class template is never a forwarding reference (since C++17))

template<class T>
int f(T&&); // P is an rvalue reference to cv-unqualified T (forwarding reference)
template<class T>
int g(const T&&); // P is an rvalue reference to cv-qualified T (not special)

int main()
{
int i;
int n1 = f(i); // argument is lvalue: calls f<int&>(int&) (special case)
int n2 = f(0); // argument is not lvalue: calls f<int>(int&&)

// int n3 = g(i); // error: deduces to g<int>(const int&&), which
// cannot bind an rvalue reference to an lvalue
}

In line 2 there is no template argument deduction going on - f2 takes an rvalue reference, and will reject anything that will not bind to that. Lvalues do not bind to rvalue references.

Why do we need reference collapsing rules

My question is why do we need these rules if T is already deduced to int&&?

This is not quite true. The rules for type deduction won't deduce the argument to be a reference. That is, in:

template <typename T>
void f(T);

And the expressions:

X g();
X& h();
X a;
f(g()); // argument is an rvalue, cannot be bound by lvalue-ref
f(h()); // argument is an lvalue
f(a); // argument is an lvalue

The deduced type will be X in last two cases and it will fail to compile in the first. The type deduced will be the value type, not a reference type.

The next step is to figure out what the deduced type would be if the template took the argument by lvalue or rvalue reference. In the case of lvalue references, the options are clear, with a modified f:

template <typename T>
void f(T &);

f(g()); // only const& can bind an rvalue: f(const X&), T == const int
f(h()); // f(X&)
f(a); // f(X&)

Up to here it was already defined in the previous version of the standard. Now the question is what should the deduced types be if the template takes an rvalue-references. This is what was added in C++11. Consider now:

template <typename T>
void f(T &&);

And rvalue will only bind to an rvalue, and never to an lvalue. This would imply that using the same simple rules as for lvalue-references (what type T would make the call compile) the second and third calls would not compile:

f(g());     // Fine, and rvalue-reference binds the rvalue
f(h()); // an rvalue-reference cannot bind an lvalue!
f(a); // an rvalue-reference cannot bind an lvalue!

Without the reference collapsing rules, the user would have to provide two overloads for the template, one that takes an rvalue-reference, another that takes an lvalue-reference. The problem is that as the number of arguments increases the number of alternatives grows exponentially, and implementing perfect forwarding becomes almost as hard in C++03 (with the only advantage of being able to detect an rvalue with an rvalue-reference).

So something different needs to be done, and that is reference collapsing, which are really a way of describing the desired semantics. A different way of describing them is that when you type && by a template argument you don't really ask for an rvalue-reference, as that would not allow the call with an lvalue, but you are rather asking the compiler to give you the best type of reference matching.

Example of rvalue reference collapsing with && &&

Forwarding when the parameter type is an unconstrained auto would be a real world example. For instance, in a lambda.

[](auto && arg) {
return foobar(std::forward<decltype(arg)>(arg));
}

decltype(arg) would be some T&& when the lambda is called with an rvalue. The reference collapsing rules would ultimately kick in inside the implementation of std::forward and turn T && && into a T&& correctly for the return type.

Reference collapsing?

In C++03, it was not legal to do the following

typedef int &ref;
ref &r = ...; // reference to reference!

This frequently causes problems for people compiling with really strict or older C++03 compilers (GCC4.1 as well as Comeau 8/4/03 do not like the above) because the Standard function object binders do not take care of the "reference to reference" situation, and occasionally create such illegal types.

In C++0x this is called "reference collapsing", yes. Most current C++03 compilers do that (i.e a T& where T denotes a reference type is T again), by retroactively applying the rule. The boost.call_traits library makes it easy to declare such function parameters though, so that the "reference to reference" situation does not occur.

Please note that the const there does not have any effect. A const applied on a reference type is silently ignored. So even if the compiler supports reference collapsing, the following is not legal

int const x = 0;

// illegal: trying to bind "int&" to "int const"!
ref const& r = x;

Collapsing of forwarding references

That's how forwarding reference works in template argument deduction:

(emphasis mine)

4) If P is an rvalue reference to a cv-unqualified template parameter (so-called forwarding reference), and the corresponding function call argument is an lvalue, the type lvalue reference to A is used in place of A for deduction (Note: this is the basis for the action of std::forward

That means, if an lvalue is passed, T will be deduced as lvalue-reference. Otherwise, if T would be deduced as the same type for both lvalue and rvalue in a forwarding reference, std::forward could not be used to perform perfect forwarding again; i.e. forwarding as lvalue when an lvalue is passed and forwarding as rvalue when an rvalue is passed.



Related Topics



Leave a reply



Submit