Understanding rvalue references
Asked Answered
L

5

23

I think there's something I'm not quite understanding about rvalue references. Why does the following fail to compile (VS2012) with the error 'foo' : cannot convert parameter 1 from 'int' to 'int &&'?

void foo(int &&) {}
void bar(int &&x) { foo(x); };

I would have assumed that the type int && would be preserved when passed from bar into foo. Why does it get transformed into int once inside the function body?

I know the answer is to use std::forward:

void bar(int &&x) { foo(std::forward<int>(x)); }

so maybe I just don't have a clear grasp on why. (Also, why not std::move?)

Lifework answered 26/9, 2012 at 16:47 Comment(2)
#6865380Obelize
You're confusing expressions, variables and references.Clan
P
17

I always remember lvalue as a value that has a name or can be addressed. Since x has a name, it is passed as an lvalue. The purpose of reference to rvalue is to allow the function to completely clobber value in any way it sees fit. If we pass x by reference as in your example, then we have no way of knowing if is safe to do this:

void foo(int &&) {}
void bar(int &&x) { 
    foo(x); 
    x.DoSomething();   // what could x be?
};

Doing foo(std::move(x)); is explicitly telling the compiler that you are done with x and no longer need it. Without that move, bad things could happen to existing code. The std::move is a safeguard.

std::forward is used for perfect forwarding in templates.

Philbin answered 26/9, 2012 at 20:3 Comment(1)
I like your explanation. The "telling the compiler we're done" part made the pieces fit together in my head better.Lifework
H
12

Why does it get transformed into int once inside the function body?

It doesn't; it's still a reference to an rvalue.

When a name appears in an expression, it's an lvalue - even if it happens to be a reference to an rvalue. It can be converted into an rvalue if the expression requires that (i.e. if its value is needed); but it can't be bound to an rvalue reference.

So as you say, in order to bind it to another rvalue reference, you have to explicitly convert it to an unnamed rvalue. std::forward and std::move are convenient ways to do that.

Also, why not std::move?

Why not indeed? That would make more sense than std::forward, which is intended for templates that don't know whether the argument is a reference.

Hexangular answered 26/9, 2012 at 17:6 Comment(0)
M
9

It's the "no name rule". Inside bar, x has a name ... x. So it's now an lvalue. Passing something to a function as an rvalue reference doesn't make it an rvalue inside the function.

If you don't see why it must be this way, ask yourself -- what is x after foo returns? (Remember, foo is free to move x.)

Meagre answered 26/9, 2012 at 17:0 Comment(1)
The type of x is still an rvalue reference because that's what it's declared as, but the expression is an lvalue. I would just change "doesn't make it an rvalue reference" to "doesn't make it an rvalue".Randalrandall
H
3

rvalue and lvalue are categories of expressions.

rvalue reference and lvalue reference are categories of references.

Inside a declaration, T x&& = <initializer expression>, the variable x has type T&&, and it can be bound to an expression (the ) which is an rvalue expression. Thus, T&& has been named rvalue reference type, because it refers to an rvalue expression.

Inside a declaration, T x& = <initializer expression>, the variable x has type T&, and it can be bound to an expression (the ) which is an lvalue expression (++). Thus, T& has been named lvalue reference type, because it can refer to an lvalue expression.

It is important then, in C++, to make a difference between the naming of an entity, that appears inside a declaration, and when this name appears inside an expression.

When a name appears inside an expression as in foo(x), the name x alone is an expression, called an id-expression. By definition, and id-expression is always an lvalue expression and an lvalue expressions can not be bound to an rvalue reference.

Hackberry answered 8/3, 2018 at 18:46 Comment(0)
U
1

When talking about rvalue references it's important to distinguish between two key unrelated steps in the lifetime of a reference - binding and value semantics.

Binding here refers to the exact way a value is matched to the parameter type when calling a function.

For example, if you have the function overloads:

void foo(int a) {}
void foo(int&& a) {}

Then when calling foo(x), the act of selecting the proper overload involves binding the value x to the parameter of foo.

rvalue references are only about binding semantics.

Inside the bodies of both foo functions the variable a acts as a regular lvalue. That is, if we rewrite the second function like this:

void foo(int&& a) {
    foo(a);
} 

then intuitively this should result in a stack overflow. But it doesn't - rvalue references are all about binding and never about value semantics. Since a is a regular lvalue inside the function body, then the first overload foo(int) will be called at that point and no stack overflow occurs. A stack overflow would only occur if we explicitly change the value type of a, e.g. by using std::move:

void foo(int&& a) {
    foo(std::move(a));
}

At this point a stack overflow will occur because of the changed value semantics.

This is in my opinion the most confusing feature of rvalue references - that the type works differently during and after binding. It's an rvalue reference when binding but it acts like an lvalue reference after that. In all respects a variable of type rvalue reference acts like a variable of type lvalue reference after binding is done.

The only difference between an lvalue and an rvalue reference comes when binding - if there is both an lvalue and rvalue overload available, then temporary objects (or rather xvalues - eXpiring values) will be preferentially bound to rvalue references:

void goo(const int& x) {}
void goo(int&& x) {}

goo(5); // this will call goo(int&&) because 5 is an xvalue

That's the only difference. Technically there is nothing stopping you from using rvalue references like lvalue references, other than convention:

void doit(int&& x) {
    x = 123;
}

int a;
doit(std::move(a));
std::cout << a; // totally valid, prints 123, but please don't do that

And the keyword here is "convention". Since rvalue references preferentially bind to temporary objects, then it's reasonable to assume that you can gut the temporary object, i.e. move away all of its data away from it, because after the call it's not accessible in any way and is going to be destroyed anyway:

std::vector<std::string> strings;
string.push_back(std::string("abc"));

In the above snippet the temporary object std::string("abc") cannot be used in any way after the statement in which it appears, because it's not bound to any variable. Therefore push_back is allowed to move away its contents instead of copying it and therefore save an extra allocation and deallocation.

That is, unless you use std::move:

std::vector<std::string> strings;
std::string mystr("abc");
string.push_back(std::move(mystr));

Now the object mystr is still accessible after the call to push_back, but push_back doesn't know this - it's still assuming that it's allowed to gut the object, because it's passed in as an rvalue reference. This is why the behavior of std::move() is one of convention and also why std::move() by itself doesn't actually do anything - in particular it doesn't do any movement. It just marks its argument as "ready to get gutted".


The final point is: rvalue references are only useful when used in tandem with lvalue references. There is no case where an rvalue argument is useful by itself (exaggerating here).

Say you have a function accepting a string:

void foo(std::string);

If the function is going to simply inspect the string and not make a copy of it, then use const&:

void foo(const std::string&);

This always avoids a copy when calling the function.

If the function is going to modify or store a copy of the string, then use pass-by-value:

void foo(std::string s);

In this case you'll receive a copy if the caller passes an lvalue and temporary objects will be constructed in-place, avoiding a copy. Then use std::move(s) if you want to store the value of s, e.g. in a member variable. Note that this will work efficiently even if the caller passes an rvalue reference, that is foo(std::move(mystring)); because std::string provides a move constructor.

Using an rvalue here is a poor choice:

void foo(std::string&&)

because it places the burden of preparing the object on the caller. In particular if the caller wants to pass a copy of a string to this function, they have to do that explicitly;

std::string s;
foo(s); // XXX: doesn't compile
foo(std::string(s)); // have to create copy manually

And if you want to pass a mutable reference to a variable, just use a regular lvalue reference:

void foo(std::string&);

Using rvalue references in this case is technically possible, but semantically improper and totally confusing.

The only, only place where an rvalue reference makes sense is in a move constructor or move assignment operator. In any other situation pass-by-value or lvalue references are usually the right choice and avoid a lot of confusion.


Note: do not confuse rvalue references with forwarding references that look exactly the same but work totally differently, as in:

template <class T>
void foo(T&& t) {
}

In the above example t looks like a rvalue reference parameter, but is actually a forwarding reference (because of the template type), which is an entirely different can of worms.

Undergrown answered 4/3, 2020 at 11:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.