C++: Return value optimization vs. std::move
ibm.com
ibm.com
foo.cpp:44:25: error: moving a temporary object prevents copy elision [-Werror,-Wpessimizing-move]
auto result = std::move(worker.run());
foo.cpp:44:25: note: remove std::move call here
auto result = std::move(worker.run()); struct X {};
void f() {
X x;
return x; // elided
}
void f(X x) {
return x; // elided
}
Perhaps not as intuitive as the first copy elision is the second, but accepting a parameter by value and returning it also enables copy elision because it is an xvalue at the end of the scope. A move is equally detrimental to performance in both cases.[1] http://eel.is/c++draft/basic.lval#xvalue [2] http://stackoverflow.com/q/3601602/1170277
No it doesn't. Parameters are constructed by the caller (in order to allow eliding the parameter copy, if the argument is a temporary anyway), therefore returning a parameter cannot be elided because that would have required the caller to know to construct the parameter in the return value slot.
If you actually test your code, you'll see that in the `f(X x)` case, the return is not elided.
With that said, in C++11, the return is automatically by move, not by copy. Any "return x;" where x is a local variable (including parameters) will be by move. But prior to C++11, it would have been a copy.
This is IMO the biggest problem with relying on RVO for anything: people rarely understand all of the rules around it, and may reasonably assume that RVO will kick in in cases where it doesn't. It's even harder if you have a function with a few branches and multiple returns -- in some cases, even when the branching happens before the return variable is declared, the compiler will fail to RVO it. Luckily C++11's "returning a local variable is automatically by move" rule covers you in most such cases.
FWIW, I highly recommend following Rust's rule: Only trivial, shallow types can be implicitly copied (i.e. only cases where the copy is a simple memcpy()). Anything else can only be moved, but is free to define a `clone()` method for explicit copies. If your type doesn't have a copy constructor then you can't accidentally call it. :)
Sorry, I confounded two things here. What I meant to say is that "x" is an xvalue in both scenarios and you wouldn't write std::move(x) in either case. For the overload f(X) you'd always incur a move, whereas for the overload f() you'd always get a copy elision.
Concretely, I've looked at the assembly of this code:
#include <string>
struct X {
std::string str;
};
auto f(X x) -> X {
return x;
}
auto f() -> X {
X x;
return x;
}
auto main() -> int {
X x = f();
auto y = f(x);
}
When compiling with c++ -g -std=c++14 test.cc && otool -tV a.out | c++filt
the assembly shows that indeed shows that f(X) invokes the move constructor, whereas the copy is elided for f(). constructor.
destructor.
move constructor
destructor.
i.e. the data destructed before it was moved.> Note: We should not use this way in the real development, because it is a reference to a local object. Here just show how to make RVO happened.
I'm struggling to see how it's an appropriate use of std::move in any case where you actually use the rvalue reference!
Another good read: http://en.cppreference.com/w/cpp/language/copy_elision
Sure, in the sense that non-zero is greater than zero. But move constructors are almost always basically a memcpy() sometimes followed by some zeroing of the old data, never heap allocation nor expensive atomic ops. IMO it's not worth worrying about. Much better to accidentally invoke a redundant move because you specified std::move() when you didn't have to than to accidentally invoke a redundant copy because you thought RVO would kick in but it didn't.
Does it? Why change the behavior specifically for std::move instead of just allowing any rvalue reference to be elided when possible?
So to elide a copy/move from an rvalue reference, the compiler would have to know at compile time that the rvalue reference always points to some particular local variable.
In theory a compiler could look into the inline definition of std::move() and determine that it always returns a reference to its parameter, but that's a rather involved optimization for the spec to require of all implementations.
std::unique_ptr<BigObject> Blerg(yourmom) {
std::unique_ptr<BigObject> big_object_ptr;
BigObject foo, bar;
if (2 > yourmom) {
big_object_ptr.reset(foo);
} else {
big_object_ptr.reset(bar);
}
return std::move(big_object_ptr);
}
This isn't valid code but it's pretty close.Unfortunately this comes at a cost. Most notably, now you're unnecessarily stressing the allocator. But worse, you no longer enjoy the benefits of value semantics [1].
[1] https://akrzemi1.wordpress.com/2012/02/03/value-semantics/
big_object_ptr.reset(...)
will cause the BigObject copy constructor to be called. The object pointed to by unique_ptr does not have the same storage as the objects "foo" and "bar". At the very least, you need to do big_object_ptr.reset(std::move(foo));
which will cause foo to be moved into the unique_ptr's storage.In code like this, you should just use make_unique, in which case the BigObject is constructed in place, and unique_ptr can be RVO'ed:
if (...) {
return make_unique<BigObject>(...);
} else ...Furthermore move constructors often have to throw an exception due to the fact that moving an object should leave the original object in a valid state. Often times leaving the original object in a valid state requires involves performing an operation that could potentially fail. In the case of std::map and std::list, a conforming implementation may have to allocate memory in order for the original object to be in a valid state.
Nonsense.
> Furthermore move constructors often have to throw an exception due to the fact that moving an object should leave the original object in a valid state.
Whether or not move constructors should leave objects in a 'valid state', whatever you mean by that, is hotly contested. Personally I'm of the opinion the only valid operations on a moved-from object should be to move-assign to it (otherwise literally every mutating algorithm is broken) and, obviously, to destroy it.
> Often times leaving the original object in a valid state requires involves performing an operation that could potentially fail.
Which is exactly why I'm of the above opinion. Making move non-throwing is more or less always trivial and cheap, whereas doing so while ensuring a moved-from object has a valid and useful state, where all class invariants have been maintained, can be expensive.
You should definitely favour a noexcept move constructor that results in an unspecified state over a implementing a throwing move. Just don't do it. If your move constructor throws then doing anything useful with your class becomes very expensive (because classes like std::vector will start copying your objects to move them around).
> In the case of std::map and std::list, a conforming implementation may have to allocate memory in order for the original object to be in a valid state.
This is just wrong. Moving a std::list will not throw provided the associated memory allocator is noexcept move constructible (and std::allocator is). The same is true of std::map.
'move' is flawed because if blurs the object id == object address equality inherited from C.
Both, RVO and 'move' are workarounds for language deficits - workarounds gone wrong.