Question

Everyone may know that we use rvalue-references combined with the reference collapsing rules to build perfect forwarding functions, like so

template<typename T>
void f(T&& arg) {
    otherfunc(std::forward<T>(arg));
}

f(4);

The reference collapsing rules are like

+------+-----+--------+
| T    | Use | Result |
|------|--------------|
| X&   | T&  | X&     |
| X&   | T&& | X&     |
| X&&  | T&  | X&     |
| X&&  | T&& | X&&    |
+------+-----+--------+

So in my example f, T is int&& and T&& is int&& && which collapses to int&&.

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

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

f(4);

turn into void f(int) instead of void f(int&&) if T is int&&? If T is really int and T&& is what makes it into int&& and therefore void f(int&&), then why do we need the reference collapsing rules since it seems that they're never applied? Those are the only two options that I can tell from my limited knowledge so obviously there is a rule I don't know of.

It would also be helpful to see a quote from the standard about this.

Was it helpful?

Solution

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.

OTHER TIPS

In your example, T is int, not int&&. void f(T arg) variant would always accept a parameter by value, making a copy. That, of course, defeats the point of perfect forwarding: otherfunc could very well be taking its parameter by reference, avoiding a copy; you could even call it with a class that is not copyable.

On the other hand, imagine that you call f() passing an lvalue, say of class C. Then T is C&, and T&& becomes C& && collapsing to C&. This way, perfect forwarding preserves "lvalue-ness", so to speak. That's what collapsing rules are for.

Consider:

#include <utility>
#include <iostream>
using namespace std;

class C {
public:
    C() : x(0) {}
    int x;
private:
    C(const C&);
};

void otherfunc(C& c) { c.x = 1; }

template<typename T>
void f(T&& arg) {
    otherfunc(std::forward<T>(arg));
}

template<typename T>
void g(T arg) {
    otherfunc(std::forward<T>(arg));
}

int main() {
    C c;
    f(c);  // OK
//  g(c);  // Error: copy constructor inaccessible

    cout << c.x;  // prints 1
    return 0;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top