1

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.

Kal
  • 1,309
  • 11
  • 16
  • The standard contains the rules, more often than not there are no rationales for those rules. – David Rodríguez - dribeas Oct 05 '13 at 03:10
  • @DavidRodríguez-dribeas even if there is no reason, I just need to know why `T` seems to be sometimes used as `int` and sometimes as `int&&`, and when those times are so I can predict the behavior of my code – Kal Oct 05 '13 at 03:10
  • @Kal: Don't get me wrong, there are reasons, it is just that you won't find it spelled in the standard (that is, the *It would also be helpful to see a quote from the standard about this*, well, the standard doesn't explain the reasons as much as it sets the rules) – David Rodríguez - dribeas Oct 05 '13 at 04:02

2 Answers2

1

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;
}
Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • Well `4` is an rvalue isn't it? So as you say, if `T` is `int` and not `int&&`, then `T&&` is simply `int&&` and the reference collapsing rules don't come in to play. – Kal Oct 05 '13 at 11:15
  • For the case of `f(4)`, indeed they don't. In my example of `f(c)`, they do. Note that it's the same function both times, only the form of the call is different. That's the point of perfect forwarding - you can write a single function template that accepts and forwards all kinds of arguments, preserving their important characteristics, such as const-ness or lvalue-ness. – Igor Tandetnik Oct 05 '13 at 13:01
  • So then in `f(4)` `T` is `int` and in `f(c)` `int` is `&`, in what case is `T` `int&&`? Would you say in the case `f(std::move(y))`? Then why is that different than `f(4)`? – Kal Oct 05 '13 at 15:03
  • In no case is `T` ever deduced to be `int&&`. You could, I suppose, explicitly call `f(4)` if you were so inclined. Why is what different than `f(4)`? I don't understand this question. – Igor Tandetnik Oct 05 '13 at 18:55
  • I think `T` can be deduced to `int&&`, or else the rule `T&& &&` would never be applied. – Kal Oct 05 '13 at 19:30
  • @Kal `T` can't be deduced to be `int&&`. That rule can be applied outside of template argument deduction: `using foo = int&&; using bar = foo&&`. Reference collapsing occurs and `bar` is `int&&`. – Simple Oct 05 '13 at 20:25
  • @Simple ah I see, if you write that as an answer I'll accept it. – Kal Oct 05 '13 at 20:34
1

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.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • For your example `f(g()); // only const& can bind an rvalue: f(const X&), T == const int` that doesn't compile for me – Kal Oct 05 '13 at 11:20
  • So what you're saying is that there's not a single rule that takes care of all cases, it's that there's different rules for each case: `T`, `T&` and `T&&` right? – Kal Oct 05 '13 at 11:20
  • @Kal: This is not the rules, but an attempt to explain why those rules are necessary/useful. Without reference collapsing you cannot implement perfect forwarding without an exponential explosion. – David Rodríguez - dribeas Oct 05 '13 at 14:49
  • I already know why the rules are useful, I do not know the rules that make these rules necessary though, or "the rules that use the rules (of reference collapsing)." That is my question. – Kal Oct 05 '13 at 14:59
  • So if you need further clarification, with the call `f(4)` why is `T` = `int` with `void f(T);` but `T` = `int&&` with `void f(T&&)`, even though the call site looks exactly the same? In both cases they are called with an x/rvalue. Why is the deduction of `T` different even though in the first case with `void f(T)`, if you set `T` = `int&&` it will be fine and legal? – Kal Oct 05 '13 at 15:06
  • @Kal: If things were as you are saying, how could you write a template that takes the argument by value? Read the beginning of the answer, the rules for argument deduction **require** that the deduced type for `f(1)` in `template void f(T);` is **int**, not `int&&`. I think you are basing your argument on the wrong premise that the type deduced is `int&&` when it is `int`. Only if you add `&&` you can get an rvalue-reference (or lvalue-reference due to reference collapsing). – David Rodríguez - dribeas Oct 05 '13 at 21:11
  • Yeah that was my problem, I thought `T` could be `int&&` with `f(4)`. Thanks. – Kal Oct 06 '13 at 00:30