This question already has answers here: C++11 rvalues and move semantics confusion (return statement) (6 answers) Closed 4 years ago.
I don't understand when I should use std::move
and when I should let the compiler optimize... for example:
using SerialBuffer = vector< unsigned char >;
// let compiler optimize it
SerialBuffer read( size_t size ) const
{
SerialBuffer buffer( size );
read( begin( buffer ), end( buffer ) );
// Return Value Optimization
return buffer;
}
// explicit move
SerialBuffer read( size_t size ) const
{
SerialBuffer buffer( size );
read( begin( buffer ), end( buffer ) );
return move( buffer );
}
Which should I use?
move
explicitly: modern compilers are smart enough to use RVO pretty much everywhere and it's more efficient than move
. But that's just "hearsay", mind you, so I'm quite interested in a documented explanation.
std::unique_ptr<base> f() { auto p = std::make_unique<derived>(); p->foo(); return p; }
, but if the types are the same it will move if possible (and that move might be elided)
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.
All return values are either already moved
or optimized out, so there is no need to explicitly move with return values.
Compilers are allowed to automatically move the return value (to optimize out the copy), and even optimize out the move!
Section 12.8 of n3337 standard draft (C++11):
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or 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): [...] Example: class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; } Thing t2 = f(); Here the criteria for elision can be combined to eliminate two calls to the copy constructor of class Thing: the copying of the local automatic object t into the temporary object for the return value of function f() and the copying of that temporary object into object t2. Effectively, the construction of the local object t can be viewed as directly initializing the global object t2, and that object’s destruction will occur at program exit. Adding a move constructor to Thing has the same effect, but it is the move construction from the temporary object to t2 that is elided. — end example ] When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, 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.
It's quite simple.
return buffer;
If you do this, then either NRVO will happen or it won't. If it doesn't happen then buffer
will be moved from.
return std::move( buffer );
If you do this, then NVRO will not happen, and buffer
will be moved from.
So there is nothing to gain by using std::move
here, and much to lose.
There is one exception* to the above rule:
Buffer read(Buffer&& buffer) { //... return std::move( buffer ); } If buffer is an rvalue reference, then you should use std::move. This is because references are not eligible for NRVO, so without std::move it would result in a copy from an lvalue. This is just an instance of the rule "always move rvalue references and forward universal references", which takes precedence over the rule "never move a return value".
* As of C++20 this exception can be forgotten. Rvalue references in return
statements are implicitly moved from, now.
If you're returning a local variable, don't use move()
. This will allow the compiler to use NRVO, and failing that, the compiler will still be allowed to perform a move (local variables become R-values within a return
statement). Using move()
in that context would simply inhibit NRVO and force the compiler to use a move (or a copy if move is unavailable). If you're returning something other than a local variable, NRVO isn't an option anyway and you should use move()
if (and only if) you intend to pilfer the object.
return v;
, in this form, NRVO will elide the move (and the copy). Under C++14, it wasn't required to perform move-elision, but it was required to perform copy-elision (necessary to support move-only types). I believe in more recent C++ standards, it is required to elide the move too (to support immobile types). If the line is instead return std::move(v);
, you are no longer returning a local variable; you're returning an expression, and NRVO isn't eligible --- a move (or copy) will be required.
std::move
and apply NRVO. Adding return std::move(v);
on line 17 empirically shows that neither move constructor nor copy-constructor are ever called (You can try by clicking on "run it" and by selecting the compiler option "gcc 4.7 C++11"). Clang however, outputs a warning but is still able to apply NRVO. So I guess it is very good practice to not add std::move
but adding it will not necessarily purely inhibit NRVO, that was my point.
vector<Noisy>
rather than a Noisy
. vector<>
's move constructor can move the contained objects via pointer manipulation so the individual objects don't have to be moved. If you change the function to use Noisy
directly rather than vector<Noisy>
, the move shows up.
vector<Noisy>
with array<Noisy,3>
. That allows you to see moves in conjunction with a container of objects, but the objects are aggregated into the data type directly as values rather than hidden behind freestore allocations that allow STL optimizations to obscure the move. (It might be a good change to make to that cppreference.com page, to more directly illustrate value-based moves and copy/move elision.)
Success story sharing
-fno-elide-constructors
) the move constructor gets called.-fno-elide-constructors
doesn't disable copy elision, it disables return value optimisation. The former is a language rule that you cannot "disable"; the latter is an optimisation that takes advantage of this rule. Indeed, my entire point was that even if return value optimisation isn't used, you still get to use move semantics, which is part of the same set of language rules.-fno-elide-constructors
: "The C++ standard allows an implementation to omit creating a temporary that is only used to initialize another object of the same type. Specifying this option disables that optimization, and forces G++ to call the copy constructor in all cases. This option also causes G++ to call trivial member functions which otherwise would be expanded inline. In C++17, the compiler is required to omit these temporaries, but this option still affects trivial member functions."-fno-elide-constructors
compilation option disabled all copy elisions, that is for return statement glvalue/prvalue object initialisers (these copy elisions are called NRVO/RVO respectively), variable prvalue object initialisers, throw expression glvalue object initialisers and catch clause glvalue object initialisers. Since C++ 17, copy elision is mandatory for return statement prvalue object initialisers and variable prvalue object initialisers, therefore the option now only disables copy elision in the remaining cases.