Why does C++11 have implicit moves for value parameters, but not for rvalue parameters?
Asked Answered
P

3

27

In C++11, value parameters (and other values) enjoy implicit move when returned:

A func(A a) {
    return a; // uses A::A(A&&) if it exists
}

At least in MSVC 2010, rvalue reference parameters need std::move:

A func(A && a) {
    return a; // uses A::A(A const&) even if A::A(A&&) exists
}

I would imagine that inside functions, an rvalue reference and a value behave similar, with the only difference that in case of values, the function itself is responsible for destruction, while for rvalue references, the responsibility is outside.

What is the motivation for treating them differently in the standard?

Pyromania answered 19/3, 2012 at 22:49 Comment(1)
I have tagged this question with C++20 because the answer changes in C++20.Hubbs
K
32

The standardization committee expended great effort in creating wording so that moves would only ever happen in exactly two circumstances:

  1. When it is clearly safe to do so.
  2. When the user explicitly asks (via std::move or a similar cast).

A value parameter will unquestionably be destroyed at the end of the function. Therefore, returning it by move is clearly safe; it can't be touched by other code after the return (not unless you're deliberately trying to break things, in which case you probably triggered undefined behavior). Therefore, it can be moved from in the return.

A && variable could be referring to a temporary. But it could be referring to an lvalue (a named variable). It is therefore not clearly safe to move from it; the original variable could be lurking around. And since you didn't explicitly ask to move from it (ie: you didn't call std::move in this function), no movement can take place.

The only time a && variable will be implicitly moved from (ie: without std::move) is when you return it. std::move<T> returns a T&&. It is legal for that return value to invoke the move constructor, because it is a return value.

Now it is very difficult to call A func(A &&a) with an lvalue without calling std::move (or an equivalent cast). So technically, it should be fine for parameters of && type to be implicitly moved from. But the standards committee wanted moves to be explicit for && types, just to make sure that movement didn't implicitly happen within the scope of this function. That is, it can't use outside-of-function knowledge about where the && comes from.

In general, you should only take parameters by && in two cases: either you're writing a move constructor (or move assignment operator, but even that can be done by value), or you're writing a forwarding function. There may be a few other cases, but you shouldn't take && to a type unless you have something special in mind. If A is a moveable type, then just take it by value.

Kilar answered 19/3, 2012 at 23:21 Comment(11)
If you have a function A f();, if you called a(f()) would a(A&&) be called (since you're calling it with a temporary)? I never really became clear on the rules about this either.Wessex
@Seth, in short, yes. Consider A f(), which returns an instance of A by value. By doing so, it guarantees neither itself nor the functions it calls can hold a reference to that instance. Now, a(f()) immediately passes that instance to a(), without giving the caller any chance to hold a reference to it itself (things would be very different if you issued A my_a; a(my_a = f()); for example). But in your situation, it's perfectly safe for a(A&&) to be called, and it actually has priority over a(const A&) IIRC.Vikkivikky
I think your answer is a little ambiguous. I was expecting a clear statement from one of: (1) && parameters must be implicitly moved, or (2) must not be implicitly moved, or else (3) it's an arbitrary choice for the compiler. I think that, in this case, the implicit move is mandatory and the MSVC version is incorrect. return x; must be implicitly rewritten as return move(x);, where x is a parameter, and x is A&&, (and A is not a reference type itself!), and the return type is A (or even A&&?). I think that's guaranteed to be safe.Churinga
(a second comment by me, to make a different observation) I don't think this makes sense: " ... But it could be referring to an lvalue (a named variable). ..." The whole point of an && parameter is that the compiler says "I don't care in any way what you do with this (as long as it's destructible after you're finished with it)". So any function with a && parameter is free to move from it at return time.Churinga
A && variable could be referring to a temporary. But it could be referring to an lvalue. This is only true if && is on a template parameter. If it is not then an lvalue cannot bind to an &&.Malanie
@NathanOliver: Type&& v = std::move(lvalue); That's perfectly legal code. v is an rvalue reference variable that refers to an lvalue.Kilar
Yes but you had to explicitly do it. It does not do it naturally and if you call std::move you are basically saying you do not care, treat it as an rvalue.Malanie
@NathanOliver: Right. But the fact that you can do it means that the compiler cannot assume that such an rvalue reference variable will always refer to a temporary. And therefore, the standards committee decided that you had to be explicit about the move for any named variable, regardless of its reference type. You may agree with the justification or not, but that's why they did it.Kilar
OK. That makes sense.Malanie
So how does the compiler "know" that it may move a named rvalue reference (as in A func(A&& a); above)? Does a static_cast<A&&>(a) (that's what std::move does) set some "hidden flag" on the returned object?Ninnette
@Florian: Because the return value of that cast (and std::move) is an xvalue: a value category that exists for the sole purpose of allowing you to explicitly move from a named lvalue. It doesn't change anything about the object itself; it just creates a reference that uses a different value category, compared to just using the name directly. It's purely notational. Movement doesn't happen with lvalues because the lvalue expression value category will not bind to function parameters of rvalue reference type.Kilar
H
10

This was fixed for C++20 by P0527 and P1825. The only way to have a function parameter bind to an rvalue reference is for the source to either be a temporary or for the caller to explicitly cast a non-temporary to an rvalue (for instance, with std::move). Therefore, this "mandatory optimization" was deemed safe.

Hubbs answered 19/10, 2019 at 1:4 Comment(1)
Automatic move from local variables and parameters: en.cppreference.com/w/cpp/language/return#NotesJehovist
B
5

In your first case, the compiler knows that a is going away and nothing will be able to cling on to it: clearly, this object can be moved from and if it is not it will be destroyed. In the second case, the rvalue reference indicates that it is permissible to move from the object and the caller doesn't expect the object to stay around. However, it is the function's choice whether it takes advantage of this permission or not and there may be reasons why the function sometimes wants to move from the argument and sometimes it doesn't want to. If the compiler were given the liberty to move off this object, there would be no way to prevent the compiler from doing so. However, using std::move(a) there is already a way to indicate that it is desired to move from the object.

The general rule in the standard is that the compiler only ever moves objects implicitly which are known to go away. When an rvalue reference comes in, the compiler doesn't really know that the object is about to away: if it was explicitly std::move()ed it actually stays around.

Beabeach answered 19/3, 2012 at 23:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.