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

4

224

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?

Nemesis answered 4/7, 2013 at 15:19 Comment(8)
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.Honeysweet
You never need explicit move for a local variable function return value. It's implicit move there.Malvia
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).Malvia
For what I know the compiler can't do any move in the first version because buffer is an lvalue, it has a name son no move constructor will be calledNemesis
@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)Packston
@JonathanWakely - interesting. Opened question: #17481518Malvia
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.Guayaquil
This question is broader than the duplicate target: per coincidence it is basically the same example, but the example is just one illustration there are many more cases not covered by the duplicated target.Jamaaljamaica
T
155

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.

Taoism answered 4/7, 2013 at 15:27 Comment(8)
Even when copy elision is disabled (-fno-elide-constructors) the move constructor gets called.Shatterproof
@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.Taoism
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."Shatterproof
@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?Taoism
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.Shatterproof
Which bug are you talking about?Shatterproof
@Maggyero: The fact that the documentation of GCC says "copying" instead of "copying or moving". The behaviour you describe in your first comment is correct and intended, but did you find it contradicting the documentation that sayd "forces to call the copy constructor"?Taoism
Yes the documentation says "copy" instead of "copy or move", but since we always talk about "copy elision" even for eliding the move construction (we don’t say "move elision") I don’t think it is harmful.Shatterproof
D
138

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.

Danilodanio answered 4/7, 2013 at 15:27 Comment(15)
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.Taoism
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).Danilodanio
@KerrekSB, "... The language is perfectly clear which kinds of constructor parameters ..." this assumes that a constructor is to be called, and we're just discussing which constructed will be called. But in the case of RVO, there needn't be any constructor called at return time. I think this is the "optional" issue.Sturtevant
Using move turns your object into a rvalue reference, so effectively you're saying that it's OK for the recipient to steal the contents of your object and leave it in some unknown (but correctly destroyable) state. For a local variable of any kind which you return, that's the case anyway. It will go out of scope immediately after the return statement. Nobody will notice if it's being stolen. Obviously the compiler knows that, so it is really "will do", not "can do".Junie
@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.Danilodanio
'never better', well, okay, I suppose that theoretically someone could be returning a non-local from a function and no longer need that non-local variable... in that situation, a manual move would be required. But in general, let the compiler handle your return results, because it has one extra trick up its sleeve that standard C++ doesn't have any keywords/functions for. =)Danilodanio
"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/…Fleam
@Fleam 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()).Danilodanio
@JaminGrey It doesn't compile for unique_ptr because unique_ptr doesn't have a suitable non-move constructor (otherwise the pointer could be owned twice). If it did, such a return statement would compile and the value returned would again not be treated as an rvalue automatically - you'd get a copy.Fleam
@balki Why did you change "allowed to" to "required"? In C++ standards (both C++11 and C++03) do say "allowed to". Copy elision is not a requirement for compilers. There is even an option that prevents (-fno-elide-constructors). I reverted to "allowed to". If you think "required" is correct, it'd be much informative if you provide some references.Dynamoelectric
Is this still true if a move constructor isn't explicitly defined for the class? i.e., if a move constructor must be implicitly created by the compiler?Dottie
@Dottie RVO and copy ellision is valid even if a move constructor doesn't exist. As far as compiler-generated vs user-provided move constructors go, it shouldn't make a difference, I think.Danilodanio
@JaminGrey Thanks for that. Updating to msvc2017 meant fixing all of my non-const reference function signatures (a la https://mcmap.net/q/77118/-default-value-to-a-parameter-while-passing-by-reference-in-c), so knowing that it'll use move constructors alleviates my performance concerns for simple functions like Thing readThing() const vs Thing& readThing(Thing& obj = Thing()) const.Dottie
There were people who wasn't agreeing until I showed them this. Apparently this behavior is not well explained in many of books I find. For what I see this behavior was same even back in ISO/IEC14882 C++ 2003 standard, or possibly even way before this.Erick
Yea, it's very old behavior - like I mentioned to someone else, return statements are very easy obvious optimization points that compilers have optimized for ages. It's just, with the adoption of move semantics, people started asking themselves, "should I use move semantics when returning from functions?", not realizing the compilers were already doing something superior to moving.Danilodanio
B
95

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.

Beat answered 18/3, 2015 at 17:27 Comment(3)
Very important exception, thank you. Just came across this in my code.Heterozygous
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.Urena
@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.Curlew
L
35

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.

Lasalle answered 4/7, 2013 at 15:27 Comment(5)
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.Tarpaulin
@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.Lasalle
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.Tarpaulin
@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.Lasalle
@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.)Lasalle

© 2022 - 2024 — McMap. All rights reserved.