ChatGPT解决这个技术问题 Extra ChatGPT

c++11 Return value optimization or move? [duplicate]

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?

From what I have read so far the general consensus seems to count on the compiler using RVO rather than 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.
You never need explicit move for a local variable function return value. It's implicit move there.
The compiler is then free to choose: If it's possible, it'll use RVO and if not, it can still do a move (and if no move is possible for the type, then it'll do a copy).
@MartinBa, never say never ;) You need an explicit move if the local variable is not the same type as the return type, e.g. 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)
For completeness, what @JonathanWakely said has been addressed in a defect report and at least recent versions of gcc and clang don't need the explicit move there.

K
Kerrek SB

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.


Even when copy elision is disabled (-fno-elide-constructors) the move constructor gets called.
@Maggyero: -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.
GCC documentation on -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."
@Maggyero: Sounds like a bug in the documentation, specifically, it sounds like the wording of the documentation wasn't updated for C++11. File a bug? @JonathanWakely?
Before C++ 17 (C++ 11 and C++ 14), the -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.
f
fury.slay

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.


I'm not particularly fond of the whole "compilers can do X" argument. The question doesn't require recourse to any compiler. It's purely about the language. And there's nothing "optional" or vague about whether "a move" happens. The language is perfectly clear which kinds of constructor parameters can bind to the return value (which is an xvalue); overload resolution does the rest.
It's not about what compilers can do, it's what the major compilers do do. Moving things explicitly might get in the way of the compilers doings things even better than moving. Any compiler that is advanced enough to allow you to explicitly move is almost certainly advanced enough to automatically move the return values - because unlike other situations where you might want to explicitly move, the return value is very easy for compilers to detect as a good place to optimize (because any return is a guarantee that the value won't be used any further in the function that is doing the return).
@Damon: Well, sortof. It compilers could move the return value (and save a copy), but they often don't. Instead they use copy-ellison wherever possible, which saves the copy and the move. They just assign directly to the variable receiving the function's result, instead of a temporary that gets returned and later assigned. Manually moving the variable is never better and often slightly (just slightly) worse than what the compiler does. The compiler falls back on move-semantics, but would rather use RVO when possible. At least, that's my understanding.
"All return values are already moved or else optimized out" Not if the types don't match: groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/…
@cdyson37 Interesting, I never encountered that corner-case before. Luckily, it doesn't even compile without the std::move(). I'm trying to figure out whether that example is actually demonstrating part of the intended language features, or taking advantage of an accidental quirk of templated member functions (in this case, the templated move constructor of std::unique_ptr()).
O
Oktalist

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.


Very important exception, thank you. Just came across this in my code.
What a funny state for a programming language to be in where one must use memory mnemonics to encode a decision tree on how to do a simple thing like return a value without copy. Are move semantics and rvalues universally held as a success of the design of cpp? They certainly are a complex solution to what seems to me to be a simple problem. Compounded with the implicit use of NVRO this certainly makes for a very confusing design.
@ldog, as for many design decisions not only with focus on c++ only, it's almost always a balance between the pros and cons. Accident manual suppression of RVO/NRVO this way seems an acceptable risk for me when taking all the pros of rvalue references into account, especially if the mistakes are done in a very explicit way via return std::move(.... And since rvalue funtion parameters are new to the language since C++11, existing former code or 'established style habits' won't be broken accidently that likely. Guaranteed copy elision since C++17 further helps to keep things here in mind.
A
Adam H. Peterson

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.


Is that correct? If I reuse the example from: en.cppreference.com/w/cpp/language/copy_elision Adding a std::move (line 17) on the return statement, does not disable copy elision. The standard actually says copy elision will omit "std::move" and copy constructors.
@ThomasLegris, I don't understand your comment. If you're talking about 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.
it seems that compilers are smart enough to remove the 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.
@ThomasLegris, okay, I see what you're seeing, but I have an alternate explanation. The move is indeed being performed, but what is moved is a 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.
@ThomasLegris, Just if you're interested, another way to see the move operations in that example is to replace 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.)