Why Does Std::Move Prevent Rvo (Return Value Optimization)

Why does std::move prevent RVO (return value optimization)?

The cases where copy and move elision is allowed is found in section 12.8 §31 of the Standard (version N3690):

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
  • [...]
  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
  • [...]

(The two cases I left out refer to the case of throwing and catching exception objects which I consider less important for optimization.)

Hence in a return statement copy elision can only occur, if the expression is the name of a local variable. If you write std::move(var), then it is not the name of a variable anymore. Therefore the compiler cannot elide the move, if it should conform to the standard.

Stephan T. Lavavej talked about this at Going Native 2013 (Alternative source) and explained exactly your situation and why to avoid std::move() here. Start watching at minute 38:04. Basically, when returning a local variable of the return type then it is usually treated as an rvalue hence enabling move by default.

Should we write `std::move` in the cases when RVO can not be done?

What happen in your example is not linked to RVO, but to the ternary operator ?. If you rewrite your example code using an if statement, the behavior of the program will be the one expected. Change foo definition to:

Test foo(int param)
{
Test test1;
Test test2;
if (param > 5)
return std::move(test2);
else
return test1;
}

will output Test(Test&&).


What happens if you write (param>5)?std::move(test1):test2 is:

  1. The ternary operator result is deduced to be a prvalue [expr.cond]/5
  2. Then test2 pass through lvalue-to-rvalue conversion which causes copy-initialization as required in [expr.cond]/6
  3. Then the move construction of the return value is elided [class.copy]/31.3

So in your example code, move elision occurs, nevertheless after the copy-initialization required to form the result of the ternary operator.

Why return value optimization does not work when return ()

This is NRVO, not RVO.

Here is the rule which allows NRVO (class.copy/31):

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv- unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

As you can see, in the case of (a), the expression is not a name (because of the added parenthesis), so NRVO is not allowed.

std::move and RVO optimizations

There's a fundamental piece of info you're missing: the standard specifically enforces that when a return statement (and a few other, less common contexts) specifies a function-local variable (such as o in your case), overload resolution to construct the return value from the argument is first performed as if the argument was an rvalue (even though it's not). Only when this fails is overload resolution done again, with the lvalue. This is covered by C++14 12.8/32; similar wording exists in C++11.

12.8/32 When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the
object to be copied is designated by an lvalue, or when the expression in a return statement is a (possibly
parenthesized) id-expression that names an object with automatic storage duration declared in the body or
parameter-declaration-clause of the innermost enclosing function or lambda-expression, overload resolution
to select the constructor for the copy is first performed as if the object were designated by an rvalue
. If
the first overload resolution fails or was not performed, or if the type of the first parameter of the selected
constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is
performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be
performed regardless of whether copy elision will occur. It determines the constructor to be called if elision
is not performed, and the selected constructor must be accessible even if the call is elided. —end note ] ...

(Emphasis mine)

So in effect, there's an unvaoidable, implicit std::move present in every return statement when returning a function-scope automatic variable.

Using std::move in a return statement is, if anything, a pessimisation. It prevents NRVO, and does not get you anything, due to the "implicitly try rvalue first" rule.

c++11 Return value optimization or move?

Use exclusively the first method:

Foo f()
{
Foo result;
mangle(result);
return result;
}

This will already allow the use of the move constructor, if one is available. In fact, a local variable can bind to an rvalue reference in a return statement precisely when copy elision is allowed.

Your second version actively prohibits copy elision. The first version is universally better.

Interaction between std::move, return value optimization and destructors


What will happen there? Is this guaranteed to work? Or can there be a situation in which the code doesn't compile or, worse, the destructor gets called twice on the same object?

As long as you're using automatic variables and temporary objects, C++ guarantees that each object constructed will be destroyed. Elisions will not break the meaning of your code; it's just an optimization.

Copy elision doesn't really elide a copy/move; it elides the existence of an object.

In pre-C++17, your code says to construct an ObjectContainer temporary, then copy/move it into the ObjectContainer return value from the function. A compiler may elide the creation of the temporary, instead constructing the return value object directly from the temporary's given parameters. In doing so, it elides away the copy/move, but it also elides away the destruction of the temporary that it doesn't create.

(in post-C++17, this code says to use the prvalue to initialize the return value object directly, so there's no possibility of a temporary being used here.)

Do I have to specify a copy constructor?

You only need to specify a copy constructor if you want the object to be copyable. If you provide a move constructor to a type, the compiler will automatically = delete the other copy/move constructors/assignment operators unless you explicitly write them.

What happens if I do?

That all depends on what you put into the copy constructor. If you do a standard copy of the value, then you've broken how your type works, since it's supposed to have unique ownership of the allocated pointer.

Or should I use std::move in the return statement?

... why would you? The return statement is given a prvalue; you never need to use std::move on prvalues.

Can we use the return value optimization when possible and fall back on move, not copy, semantics when not?

When the expression in the return statement is a non-volatile automatic duration object, and not a function or catch-clause parameter, with the same cv-unqualified type as the function return type, the resulting copy/move is eligible for copy elision. The standard also goes on to say that, if the only reason copy elision was forbidden was that the source object was a function parameter, and if the compiler is unable to elide a copy, the overload resolution for the copy should be done as if the expression was an rvalue. Thus, it would prefer the move constructor.

OTOH, since you are using the ternary expression, none of the conditions hold and you are stuck with a regular copy. Changing your code to

if(b)
return x;
return y;

calls the move constructor.

Note that there is a distinction between RVO and copy elision - copy elision is what the standard allows, while RVO is a technique commonly used to elide copies in a subset of the cases where the standard allows copy elision.

Why are the RVO requirements so restrictive?


auto foo() -> T&&;

auto test() -> T
{
return foo();
}

You say in this case the RVO should be allowed to be applied. However consider this legal implementation of foo:

T val;

auto foo() -> T&&
{
return static_cast<T&&>(val); // because yes, it's legal
}

The moral: only with prvalues you know for sure you have a temporary and most important you know the exact lifetime of the temporary so you can elide its construction and destruction. But with xvalues (e.g. T&& return) you don't know if that is indeed bound to a temporary, you don't know when that value was created and when it goes out of scope or even if you know you can't change it's construction and destruction point like the above example.

I'm not sure that I fully understand. If RVO were allowed to be
applied to test(), why would that be worse than if test did: T temp = foo(); return temp; which would allow NRVO?

It's not that it is worse. It is just not possible. With your example temp is a local variable in the function where you want to apply NRVO i.e. test. As such it is an object fully "known" in the context of test, its lifetime is known, the normal point of ctor and dtor is known. So instead of creating the temp variable in the stack frame of test it is created in the stack frame of the caller. This means there is no copy of the object from the stack frame of test to the stack frame of the caller. Also please see that in this example foo() is completely irrelevant. It could have been anything in the initialization of temp:

auto test() -> T
{
T temp = /*whatever*/;

return temp; // NRVO allowed
}

But with return foo() you can't elide the copy simply because you can't know to what object the return reference binds to. It can be a reference to any object.

C++: should I explicitly use std::move() in a return statement to force a move?

You should prefer

std::vector<int> foo() {
std::vector<int> v(100000,1);
return v; // move or NRVO
}

over

std::vector<int> foo() {
std::vector<int> v(100000,1);
return std::move(v); // move
}

The second snippet prevent NRVO, and in worst case both would move construct.



Related Topics



Leave a reply



Submit