clang 3.7 has a warning when std::move prevents it from using return value optimization:
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());
Doing so would require the standard to give std::move special treatment, which sucks. The way it's specified right now let's you write your own std::move and call it rvalue_cast() if you wish.
To elide a return copy/move, the compiler has to know at compile time exactly which local variable is going to be returned, so that it can arrange to have that local variable constructed directly into the return location, thus avoiding the need to copy/move it later.
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.
There's never a place where RVO could occur where `return std::move(...)` is a sensible thing to write, so it's not actually a problem in practice. If you're returning a local you don't have to explicitly move from it, and if you're returning a global or member variable RVO can't occur.
Someone correct me if I am wrong but isn't that last snippet where he returns std::move(localObj) essentially returning an rvalue-ref to a local object? That's like a big no-no because the local object destructs after foo(1) returns.
It's definitely an appropriate use of std::move. When the local object destructs std::move has already ripped its guts out and put them in the object up the stack. The local object is just an empty shell at destruction.
The 'object up the stack' is an rvalue reference. While we've successfully returned a reference into the caller's stack frame (RVO), the object it referred to has already been destroyed. That's why you see 'constructor, destructor' before 'move constructor'.
I'm struggling to see how it's an appropriate use of std::move in any case where you actually use the rvalue reference!
It'd be a problem if the function returned BigObject&&, but since it returns BigObject the move happens inside the function body which should be fine (modulo the issues raised in the article).
Another way to frame this discussion would be in terms of xvalues. The standard defines an xvalue as an "eXpiring value", which is value near the end of it's lifetime [1,2].
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.
> accepting a parameter by value and returning it also enables copy elision
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. :)
> 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.
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().
Remember, the most important thing to take away from RVO is that copy and move constructors should not have side-effects that change the behaviour of your program (except for throwing an exception if they fail, and move constructors should be designed never to throw)
The whole point of move constructors are observable side-effects.
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.
> The whole point of move constructors are observable side-effects.
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.
std::list's move constructor is deliberately not specified to be noexcept even when the allocator's is, and in Dinkumware's implementation it actually can throw, since it allocates a new end sentinel for the moved-from list.
Constructors may need to allocate a sentinel, I see no reason for the move constructor to do so. I'd also question the performance benefits of having a sentinel (basically avoiding additional null pointer checks) has on performance given todays relative cache penalties.
Instead of trying to reason about the copying and moving of huge objects, I give my brain a break by using std::unique_ptr<HugeObject>. unique_ptr is moveable, cannot accidentally be copied (because its copy constructor is private), and is trivially small. unique_ptr also gets around the problem where the compiler can't figure out which value is being RVO'd, because you can do this:
This doesn't help to solve the original problem of avoiding the copy. Your calls to
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 ...
Yeah I realized after the fact that the example is pointless, but I was trying to adhere to the original post, which is also pointless. Why would you construct two BigObject when you know you will discard one of them unused? It's dumb.
You can actually just do
return big_object_ptr;
Instead of the std::move.
The compiler will complain if it can't do RVO because unique_ptr prohibits a copy, so you're safe :)
> I give my brain a break by using std::unique_ptr<HugeObject>.
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].
Copy elision + move semantics can be quite confusing at first in C++. For example at times explicitly calling move can be less efficient than implicitly relying on copy elision.
> explicitly calling move can be less efficient than implicitly relying on 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.
40 comments
[ 3.4 ms ] story [ 83.2 ms ] threadDoes 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.
I'm struggling to see how it's an appropriate use of std::move in any case where you actually use the rvalue reference!
> 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.
[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:
When compiling with the assembly shows that indeed shows that f(X) invokes the move constructor, whereas the copy is elided for f().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.
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:
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/
'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.
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.