1

I'm currently learning perfect forwarding in c++ and I came across something that confused me. Pretty sure it's something stupid. When I used std::forward on a lvalue, it used the rvalue function. This is a pretty bad explanation so I'm just gonna show the code.

#include <iostream>

void check(int&& other) {
    std::cout << "Rvalue" << std::endl;
}

void check(int& other) {
    std::cout << "Lvalue" << std::endl;
}

void call(int& other) {
    check(std::forward<int>(other));
}

int main()
{
    int i = 4;
    call(i);
}

This outputs "Rvalue". Please help me understand why.

IDK
  • 23
  • 4
  • 1
    The _only_ reason to use `std::forward` is on a method that accepts universal template references `template void func(T&& other)`. That being said, this question is well written and worth answering. – Mooing Duck May 20 '21 at 01:31
  • Does this answer your question? [What's the difference between std::move and std::forward](https://stackoverflow.com/questions/9671749/whats-the-difference-between-stdmove-and-stdforward) There's an answer of mine there where I make the same explanation steps for `std::forward` and `std::move`, which I think gives a lot of insight on both. – Enlico May 20 '21 at 04:50

2 Answers2

3

"Perfect forwarding" is used in templates, i.e.:

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

Outside of template context the end result drastically changes. All that std::forward does is return static_cast<T &&>, so your call function becomes nothing more than:

void call(int& other) {
    check(static_cast<int &&>(other));
}

Hence you get an rvalue. The reason this works differently in templates is because a && template parameter is a forwarding reference (a fancy term for either an lvalue or an rvalue-deduced reference, depending on what gets dropped in that parameter), and because of reference collapsing rules. Briefly, when used in a template context, the end result is:

  1. T gets deduced as either an lvalue or an rvalue reference, depending on what the parameter is.

  2. The result of the static_cast<T &&> is an lvalue reference, if T is an lvalue reference, or an rvalue reference if T is an rvalue reference, due to reference collapsing rules.

The end result is that the same kind of a reference gets forwarded. But this only works in template context, since it requires both forwarding reference semantics and reference collapsing rules to work just right.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
1

std::forward() isn't exactly magic. Which is one of the reasons one has to give it the appropriate type with the appropriate reference-category (rvalue-reference, lvalue-reference, no reference).

Normally, you get the type from the template-argument. If it's an auto&&-argument (C++14 lambda, C++20 abbreviated template or use of concept), one uses decltype() to get it from the function-argument.

Manually specifying it works, but goes against the spirit of using the function. std::move() or the argument itself is much easier to use in that case, depending on the template-argument you specify.

And due to how reference-collapsing and std::forward() are defined, Type and Type&& both result in an rvalue-reference, while Type& results in an lvalue-reference.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118