Why are rvalues references variables not rvalue? [duplicate]
Asked Answered
B

3

26

Let's say I have two overloads of a function f

void f(T&&); // #1
void f(T&);  // #2

Then in the body of another function g

void g(T&& t) 
{ 
  f(t);  // calls #2
}

the overload f(T&) will be called because t is considered an lvalue.

This is very surprising to me. How a function with signature f(T&&) cannot match a call with type T&&? What surprises me even more is that a call f(static_cast<T&&>(t)) would actually call the rvalue overload f(T&&).

What are the C++ rules that make this possible? Is T&& more than a type?

Bdellium answered 17/9, 2015 at 0:20 Comment(2)
Rvalues don't have names. What you have is a name for something that you're confident is safe to move from.Haunted
This is very surprising to me. This should not be surprising.Lubricate
S
23

The things that are automatically treated as rvalues are things without names, and things that (very shortly) will not have a name (in the return value case).

T&& t has a name, it is t.

The reason why rvalues are those things is that referring to them after that point of use is next to impossible.

T&& is the type rvalue reference. An rvalue reference can only bind to an rvalue (without a static_cast being involved), but it is otherwise an lvalue of type rvalue reference.

The fact it is of type rvalue reference only matters during its construction, and if you do decltype(variable_name). It is otherwise just another lvalue of reference type.

std::move(t) does a return static_cast<T&&>(t); and returns an rvalue reference.

The rules that govern this are written in standardese in the C++ standard. A copy/paste of them won't be all that useful, because they are not that easy to understand.

The first general rule is, you get an implicit move (aka, parameter that binds to an rvalue reference argument) when you return a named value from a function, or when a value has no name, or when a function explicitly returns an rvalue reference.

Second, that only rvalue references and const& can bind to rvalues.

Third, reference lifetime extension on temporary values occurs when directly bound to a reference outside of a constructor. (as only rvalue references and const& can directly bind to a temporary, this only applies to them)

Forth, T&& isn't always an rvalue reference. If T is of type X& or X const&, then reference collapsing turns T&& into X& or X const&.

Finally, T&& in a type deduction context will deduce T as X, X&, X const& or X const&& depending on the type of the argument, and hence can act as a "forwarding reference".

Safko answered 17/9, 2015 at 0:32 Comment(2)
What do you mean by "An rvalue reference can only bind to an rvalue without a static_cast"?Bdellium
@Bdellium bind, during constructionSafko
A
11

When you refer to a variable by name, you always get an lvalue. There are no exceptions to this rule, although note that it does not apply to preprocessor macros, enumerators, or non-type template parameters, none of which are variables in the usual sense.

I argue that while this behaviour at first seems not to make sense, when you consider it more carefully, it does make sense and is the correct behaviour. First, we should observe that value category is clearly a property of expressions, and not of objects themselves. This is obvious since std::move never creates a new object, but just creates an rvalue expression referring to the given object. Then we should understand that:

  • If an expression is an lvalue, it usually means the value of the object referred to by the expression can or will be accessed through the same expression later within the same scope. This is the default assumption when the object is accessed through a named variable.
  • If an expression is an rvalue, it usually means that the value of the object referred to by the expression cannot or will not be accessed through the same expression later within the same scope. (This includes prvalue temporaries; T{} in one expression is distinct from T{} in a later expression; both create anonymous objects, but both are distinct, so the latter does not access the same object as the former.)

The value category of an expression referring to an object is therefore relative; it depends on the particular expression and scope. std::move signals your intent to not access the value of an object again in the same scope, allowing the called function to move its value out of that object. However, when the called function accesses the name of the rvalue reference, the value is permanent during the function call; the function may move the value out of that object at any point, or not at all, but at any rate it probably will access it within the body, which is after the parameters are initialized.

In this example:

void f(Foo&& foo) { /* use foo */ }
void g() {
    Foo foo;
    f(std::move(foo));
}

although std::move(foo) in g and the param foo in the callee refer to the same object, that object's value is about to disappear at the point of std::move in g, whereas in f, the value of that object is expected to be accessed through foo, possibly multiple times before the end of f.

A similar situation exists when calling ref-qualified member functions.

struct Foo {
    void f() &;
    void f() &&;
    void g() && {
        f(); // calls lvalue-qualified f
    }
};
void h() {
    Foo().g();
}

Here, Foo()'s value is about to disappear from h(); it will not be available after the full-expression. However, in the body of Foo::g(), it is permanent until the end of g(); *this reliably accesses the value of the same object. So it is natural that when g() calls f(), it should call the overload expecting an lvalue, and f() should not steal the value of *this* from g() since g() might still want to access it.

Addiel answered 17/9, 2015 at 1:1 Comment(0)
R
5

In g() t is a named variable. All named variables are lvalues. If T is a template type then you can forward the variable to f() using std::forward. This will call f() with the same type as was passed to g()

template<typename T>
g(T&& t) { f(std::forward<T>(t));}

If T is not a template type but just a type then you can use std::move

g(T&& t) { f(std:move(t)); }
Relieve answered 17/9, 2015 at 0:31 Comment(1)
Sorry but I didn't get the difference. What is "not a template type"? With OR without templates, my code ideone.com/NL0EDv seems to have no issues in using std::move, then why use forward at all?Throng

© 2022 - 2024 — McMap. All rights reserved.