views:

224

answers:

3

In perfect forwarding, std::forward is used to convert the named rvalue reference t1 and t2 to unnamed rvalue reference. What is the purpose of doing that? How would that effect the called function inner if we leave t1 & t2 as lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
+1  A: 

How would that affect the called function inner if we leave t1 & t2 as lvalue?

If, after instantiating, T1 is of type char, and T2 is of a class, you want to pass t1 per copy and t2 per const reference. Well, unless inner() takes them per non-const reference, that is, in which case you want to do so, too.

Try to write a set of outer() functions which implement this without rvalue references, deducing the right way to pass the arguments from inner()'s type. I think you'll need something 2^2 of them, pretty hefty template-meta stuff to deduce the arguments, and a lot of time to get this right for all cases.

And then someone comes along with an inner() that takes arguments per pointer. I think that now makes 3^2. (Or 4^2. Hell, I can't be bothered to try to think whether const pointer would make a difference.)

And then imagine you want to do this for a five parameters. Or seven.

Now you know why some bright minds came up with "perfect forwarding": It makes the compiler do all this for you.

sbi
+22  A: 

You have to understand the forwarding problem. You can read the entire problem in detail, but I'll summarize.

Basically, given the expression E(a, b, ... , c), we want the expression f(a, b, ... , c) to be equivalent. In C++03, this is impossible. There are many attempts, but they all fail in some regard.


The simplest is to use an lvalue-reference:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

But this fails to handle immutable values: f(1, 2, 3);, as those cannot be bound to an lvalue-reference.

The next attempt might be:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Which fixes the above problem, but flips flops. It now fails to allow E to have mutable arguments: void E(int&, int&, int&); f(1, 2, 3);.

A third solution handles everything correctly...at the cost of being impossible to maintain. You give overloads of f, with all combinations of const and non-const:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N arguments require 2N combinations, a nightmare. We'd like to do this automatically.

The last attempt accepts const-references, but then const_cast's the const away. This accepts all values, can pass on all values, but leads to potential undefined behavior:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Consider: E(int&, int&, int&); f(1, 2, 3); // ouch.


In C++0x, we get a chance to fix this. One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code.

The solution is to instead use the newly added rvalue-references; we can introduce new rules when determining reference types and create the right kind of template argument deduction (after all, we can't break any code).

If given a reference to a reference (note reference is an encompassing term meaning both T& and T&&), we use the following rule:

"[given] a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type TR."

Or in tabular form:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Next, with template argument deduction: if a parameter is an lvalue A, we supply the template argument with an lvalue reference to A. Otherwise, we deduce normally.

Why is this useful? Because combined we maintain the ability to keep track of the value category of a type: if it was an lvalue, we have an lvalue-reference parameter, otherwise we have an rvalue-reference parameter. In code:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

The last thing is to "forward" the value category of the variable. Keep in mind, once inside the function the parameter could be passed as an lvalue to anything:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

That's no good. The solution is this:

static_cast<T&&>(x);

What does this do? Consider we're inside the deduce function, and we've been passed an lvalue. This means T is a A&, and so the target type for the static cast is A& &&, or just A&. Since x is already an A&, we do nothing and are left with an lvalue reference.

When we've been passed an rvalue, T is A, so the target type for the static cast is A&&. The cast results in an rvalue expression, which can no longer be passed to an lvalue reference. We've maintained the value category of the parameter.

Putting these together gives us "perfect forwarding":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

When f receives an lvalue, E gets an lvalue. When f receives an rvalue, E gets an rvalue. Perfect.


And of course, we want to get rid of the ugly. static_cast<T&&> is cryptic and weird to remember; let's instead make a utility function called forward, which does the same thing:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
GMan
Well, GMan, I think this beats the hell out of my tiny, clumsy, incorrect attempt at answering this, so `+1` from me. `:)`
sbi
Wouldn't `f` be a function, and not an expression?
Michael Foukarakis
@sbi: Haha. :) <3 @mfukar: It's a function call expression. `f` itself could be anything "callable"; a function, function pointer, or function object.
GMan
@Potato: That is better, thanks.
GMan
Steveng
Johannes Schaub - litb
Johannes Schaub - litb
Sorry for the slow reply. @Steveng: Yes, I was incorrect, as Johannes explained. @Johannes: Thanks for the fixes (and duh on my part for `A` being deduced to `const int` in the first part, just wasn't thinking.) I'll admit I haven't even read the new taxonomy or deduction rules. :S (Just did, not too bad.) Is this correct?
GMan
@GMan i like it now =)
Johannes Schaub - litb
@Johannes: Phew. :)
GMan
+1  A: 

In perfect forwarding, std::forward is used to convert the named rvalue reference t1 and t2 to unnamed rvalue reference. What is the purpose of doing that? How would that effect the called function inner if we leave t1 & t2 as lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

If you use a named rvalue reference in an expression it is actually an lvalue (because you refer to the object by name). Consider the following example:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Now, if we call outer like this

outer(17,29);

we would like 17 and 29 to be forwarded to #2 because 17 and 29 are integer literals and as such rvalues. But since t1 and t2 in the expression inner(t1,t2); are lvalues, you'd be invoking #1 instead of #2. That's why we need to turn the references back into unnamed references with std::forward. So, t1 in outer is always an lvalue expression while forward<T1>(t1) may be an rvalue expression depending on T1. The latter is only an lvalue expression if T1 is an lvalue reference. And T1 is only deduced to be an lvalue reference in case the first argument to outer was an lvalue expression.

sellibitze