1

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.

Alexey Starinsky
  • 3,699
  • 3
  • 21
  • 57

3 Answers3

10

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.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Probably I figured this out. We can say that r-value `1` becomes l-value inside `deduce` because it gets name `x`. And the same with `foo2`, see EDIT1. – Alexey Starinsky Aug 02 '22 at 22:18
  • 2
    @AlexeyStarinsky I don't think it it helpful to say "become" here. The whole point is that value category is not a property of an object. It is not like anything changes about the object when it is passed to the function. `1` is just one expression and `x` is another, unrelated, expression. The former is a prvalue expression or a xvalue expression after temporary materialization and the latter is a lvalue expression. There is no connection between the two. That you are using the former as an argument for the function parameter named `x` is irrelevant. – user17732522 Aug 02 '22 at 22:23
  • How can you define the term `entity`?. Is there a definition in C++ standard? – Alexey Starinsky Aug 02 '22 at 22:25
  • 3
    @AlexeyStarinsky https://eel.is/c++draft/basic#pre-3 – user17732522 Aug 02 '22 at 22:26
  • 1
    @AlexeyStarinsky It is not all that relevant here though. The relevant distinction is that a _variable_ can either be an _object_ or be a _reference_, which are not _objects_ themselves. (see [pre]/6) I think this notation might not be obvious and it might be unclear that the _object_ referred to by the _result_ of the expression `x` would have likely type `int`, although the _variable_ named in the expression `x` is of type `int&&`. I like to use the more general term _entity_ to (hopefully) make it clearer that I mean the type of the reference itself, not the object it references. – user17732522 Aug 02 '22 at 22:32
  • At least I probably understand that there is a difference between `x` variable type that is `int&&` and expression `x` that is `l-value`. – Alexey Starinsky Aug 02 '22 at 23:14
2

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:

Den-Jason
  • 2,395
  • 1
  • 22
  • 17
  • I also got the same effect with `void foo2(int&& val)`, see EDIT1 – Alexey Starinsky Aug 02 '22 at 22:07
  • The "not an rvalue reference" thing doesn't sound right to me. It is one. It might ultimately resolve to an lvalue reference, but this is not unique to forwarding references, any rvalue references to dependent types (or just typedefs) can do this. – HolyBlackCat Aug 02 '22 at 23:22
0

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.

lorro
  • 10,687
  • 23
  • 36