Due to the fact that the general question "how std::forward
works" is too complicated I decided to start with a question on a particular example:
#include <utility>
#include <iostream>
namespace
{
void foo(const int&)
{
std::cout << "const l-value" << std::endl;
}
void foo(int&)
{
std::cout << "l-value" << std::endl;
}
void foo(int&&)
{
std::cout << "r-value" << std::endl;
}
template <typename T>
void deduce(T&& x)
{
//foo(std::forward<T>(x));
foo(x);
}
}
int main()
{
deduce(1);
return 0;
}
The output:
l-value
Why?
We pass r-value and the argument of deduce
is r-value, right? Why is foo(int&)
called?
When I call directly
foo(1);
it prints r-value
. When does it become l-value inside deduce
?
EDIT1
If I add foo2
to the code above:
void foo2(int&& val)
{
static_assert(std::is_same_v<decltype(val), int&&>);
foo(val);
}
and then call it from main
foo2(1);
l-value
is printed but not r-value
as I probably expected.
So, as mentioned in comments and answers, val
is l-value because it is a named variable and it does not matter if it is declared as int&&
or int&
. That is something that I did not know before.
CodePudding user response:
The thing that everyone seems to be confused about when being introduced to this is that value category is not a property of an object, variable or other entity. It is not part of a type or anything, which makes it confusing because we also talk about lvalue references and rvalue references which are distinct concepts from value categories. Value category is a property of individual expressions only.
An id-expression like x
or val
, naming a variable, is always a lvalue expression, no matter what type the named variable has or whether it is a reference or whether it is a local/global variable or a function parameter or a template parameter, etc. The expression naming a variable of rvalue reference type is therefore still a lvalue expression.
And the second kind of property of expressions, their types, have references stripped. So the value category of the expression x
(or val
) is lvalue and its type is int
, although the type of the variable named x
(or val
) is int&&
(and doesn't have any value category).
A function call expression like std::forward<T>(x)
in which the return type of the selected function is a rvalue reference is a xvalue expression (a kind of rvalue expression). And again, the expression's type is always non-reference, here int
.
For a full list of the value categories of every kind of expression, see the lists at https://en.cppreference.com/w/cpp/language/value_category.
CodePudding user response:
This
template <typename T>
void deduce(T&& x)
may casually look like a RValue reference, however it is not a RValue reference. (I misunderstood this for years until I attended a class on Universal References recently). When used in this way it is a Universal Reference.
It's subtly yet another one of those double-meanings of operators that you see elsewhere in C .... << >> | "inline" etc...
See here for more detail: https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
This is an important snippet from there:
Given that rvalue references are declared using “&&”, it seems reasonable to assume that the presence of “&&” in a type declaration indicates an rvalue reference. That is not the case:
Widget&& var1 = someWidget; // here, “&&” means rvalue reference auto&& var2 = var1; // here, “&&” does not mean rvalue reference template<typename T> void f(std::vector<T>&& param); // here, “&&” means rvalue reference template<typename T> void f(T&& param); // here, “&&”does not mean rvalue reference
To further trip you up, C 20 has a feature called "abbreviated function templates", keep an eye out for this; the above template parameter could be written as follows:
void deduce(auto&& x)
The other part of your question is "how does std::forward work". Quintessentially, it works on the basis of "reference collapsing". See a couple of articles here:
CodePudding user response:
Consider the opposite: if it weren't 'change back'. In that case, if you were to use two foo(x)
calls, the code would consider both times the T&&
overload. Therefore, you couldn't specify where you can move from it.
Hence the standard is worded in a way that, when you write std::forward<T>(x)
, you'll be able to move from it; otherwise, it's a reference (lvalue). Technically, you're searching for the terms lvalue and xvalue in the standard.