Home > OS >  std::forward through an example
std::forward through an example

Time:12-10

I would like to go over an example using std::forward because sometimes I can make it work and some other times I can’t. This is the code

void f(int&& int1, int&& int2){
    std::cout << "f called!\n";
}

template <class T>
void wrapper(T&& int1, T&& int2){
    f(std::forward<T>(int1), std::forward<T>(int2));
}


int main(){
    int int1 = 20;
    int int2 = 30;
    int &int3 = int1;
    wrapper(int1, int2);
}

  1. I am passing int 1 and int 2. These are lvalues. They are silently converted to &int1, &int2. These are converted using &&. But reference collapsing keeps them just &int1, &int2.
  2. f takes && parameters
  3. If I pass simply int1 and int2 as they are I am passing &int1, &int2. This does not work.
  4. So I pass std::forward(int1) std::forward(int2).It should be the same as using static_cast<T&&>. Because of this, thanks to referencing collapsing I can pass to every function f (theoretically even one that accepts only l-value references).
  5. My code does not compile and my logical reasoning has probably some contradictions.
candidate function not viable: no known conversion from 'int' to 'int &&' for 1st argument
void f(int&& int1, int&& int2){

How on earth did I get a simple int after using all these ampersands?

Additional question: My compiler asks me to use wrapper<int &> instead of only wrapper(some parameters). Can I just leave it like in my code, or I need to manually put wrapper<int &> (this is what my compiler is asking me to add). Why do I need <int &> int this case?

CodePudding user response:

The whole problem stems from the forwarding references using same symbols as rvalue ones, but not being the same.

Take the following code:

template<typename T>
void f(T&& t)
{
//whatever
}

In this case T&& is a forwarding reference. It is neither T&& in the sense of rvalue-reference (reference to temporary), nor is it T&. Compiler deduces that at compile time. Notice though, it does not have type specified either, it's a template paremeter. Only in case of those parameters the forwarding reference semantics applies (another way of writing it down is a auto&& in lambdas, but the deduction works the same way). Thus when you call int x= 3; f(x); you're effectively calling f(int&). Calling f(3) calls effectively f(int&&) though.

void g(int&& arg)

arg is and rvalue reference to int. Because the type is specified, it's not a template argument! It's always an rvalue reference.

Thus, for your case

void f(int&& int1, int&& int2){
    std::cout << "f called!\n";
}

template <class T>
void wrapper(T&& int1, T&& int2){
    f(std::forward<T>(int1), std::forward<T>(int2));
}


int main(){
    int int1 = 20; 
    int int2 = 30;
    int &int3 = int1;
    wrapper(int1, int2); //call wrapper(int&, int&);
    //Then f(std::forward<T>(int1), std::forward<T>(int2));-> f(int&, int&), because they are lvalues! Thus, not found, compilation error!
}

Live demo: https://godbolt.org/z/xjTnjcqj8

CodePudding user response:

forward is used to convert the parameter back to the "valueness" it had when passed to the function. We can see how this works using this example

void foo(int&) { std::cout << "foo(int&)\n"; }
void foo(int&&) { std::cout << "foo(int&&)\n"; }

template <typename T> void wrapper(T&& var) { foo(std::forward<T>(var)); }

int main()
{
    int bar = 42;
    wrapper(bar);
    wrapper(42);
}

which outputs

foo(int&)
foo(int&&)

So, when you pass wrapper an lvalue, forward will forward that along, and the lvalue accepting overload of foo is called. When you pass an rvalue to wrapper, forward will convert var back into an rvalue1 and the rvalue overload of foo is called.

Since your f function only accepts rvalues, that means your wrapper function will also only work for rvalues. You are basically just trying to do f(int1, int2) in main, and that wont work.

The reason you get the error message no known conversion from 'int' to 'int &&' is that it is trying to tell you that there is no conversion from an lvalue int into a reference to an rvalue int.

1: This is needed because as a named variable, it is an lvalue, even if it is a reference to an rvalue.

CodePudding user response:

I'll start off with something mentioned in the comments, and that is that having only one template parameter here relates the two parameters and interferes with the usual deduction process. Normally when forwarding, you deduce a type for each forwarded parameter independently. In this case, it will still work, but only because the call site passes two things with the same type and the same value category (lvalue, rvalue, etc.).

Here's what a typical wrapper would look like, truly forwarding the arguments independently:

template <class T, class T2>
void wrapper(T&& int1, T2&& int2){
    f(std::forward<T>(int1), std::forward<T2>(int2));
}

With that out of the way, I'll move on to the intuitive reason your code doesn't compile. If forwarding is done correctly, the wrapper function's existence won't change anything. By the above reasoning, your wrapper function meets this criteria for this particular call. Let's see what happens when it's gone:

 void f(int&& int1, int&& int2){
    std::cout << "f called!\n";
}

int main(){
    int int1 = 20;
    int int2 = 30;
    int &int3 = int1;
    f(int1, int2);
}

You might be able to spot the error more clearly now. f takes rvalues, but it's being given lvalues. The wrapper preserves the value category of its arguments, so they're still lvalues when handed to f. To fix this with the wrapper, it's the same as without—pass rvalues:

int main(){
    f(20, 30);
    // Alternatively: f(std::move(int1), std::move(int2));
}

Now, reviewing the points made:

I am passing int 1 and int 2. These are lvalues. They are silently converted to &int1, &int2. These are converted using &&. But reference collapsing keeps them just &int1, &int2.

Don't think about the variables themselves here, but the types. Because you pass in two lvalue ints, T is deduced to be int&. Following that, the actual parameter types, T&&, are then int& as well because of reference collapsing. Thus, you have a function stamped out with two int& parameters. In the forward calls, T is int&, so its reference-collapsed return type is again int&. Thus, the call expressions have the type int and are lvalues (specifically because the return type is int&—the language calls that out as an explicit rule).

As a side note, &int1 isn't clear to me because its C meaning is taking the address of int1, an entirely irrelevant meaning here. I think what you're trying to say is that it's an lvalue or that a parameter's type is an lvalue reference.

If I pass simply int1 and int2 as they are I am passing &int1, &int2. This does not work.

This is true for the the reasons discussed earlier. The parameters are themselves lvalues, so a function taking rvalues won't accept them.

So I pass std::forward(int1) std::forward(int2).It should be the same as using static_cast<T&&>. Because of this, thanks to referencing collapsing I can pass to every function f (theoretically even one that accepts only l-value references).

Yes, this is the point of forwarding. It lets you preserve the value category of the arguments given to the wrapper, so the same template can be used to pass along arguments to a function regardless of which value categories it accepts. It's up to the caller of the wrapper to provide the correct value category in the first place and then the wrapper simply promises not to mess with it.

How on earth did I get a simple int after using all these ampersands?

This is just how the type is displayed in the error message. Although the parameter is an int&, the expression has the type int. Remember that an expression has both a type and a value category and that they're separate properties. For example, you can have an rvalue reference parameter int&& x and the expression x will still be an int and an lvalue.

  •  Tags:  
  • c
  • Related