
C++: Return value optimization vs. std::move - ingve
https://www.ibm.com/developerworks/community/blogs/5894415f-be62-4bc0-81c5-3956e82276f3/entry/RVO_V_S_std_move?lang=en
======
lorenzhs
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());

~~~
arto
That's a welcome feature indeed. (Too bad the 3.7 release is still a month
away.)

------
mavam
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].

    
    
      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](http://eel.is/c++draft/basic.lval#xvalue)
[2]
[http://stackoverflow.com/q/3601602/1170277](http://stackoverflow.com/q/3601602/1170277)

~~~
kentonv
> 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. :)

~~~
mavam
> 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().

------
Sirenos
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.

~~~
a8da6b0c91d
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.

~~~
seabee
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!

------
shmerl
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.

Another good read:
[http://en.cppreference.com/w/cpp/language/copy_elision](http://en.cppreference.com/w/cpp/language/copy_elision)

~~~
kentonv
> 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.

~~~
shmerl
That may be, though code becomes more cluttered with moves as well.

------
im3w1l
Sounds like a "specification bug" that RVO is not allowed when std::move is
used. Or is there a deeper reason for this?

~~~
nly
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.

~~~
AnthonyMouse
> Doing so would require the standard to give std::move special treatment

Does it? Why change the behavior specifically for std::move instead of just
allowing any rvalue reference to be elided when possible?

~~~
kentonv
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.

------
thrownaway2424
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:

    
    
      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.

~~~
mgraczyk
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 ...

~~~
thrownaway2424
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.

------
nly
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)

~~~
Kranar
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.

~~~
nly
> 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.

~~~
plorkyeran
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.

~~~
nly
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.

------
ExpiredLink
RVO is flawed because the compiler may(!) elide function calls. The compiler
cannot know if those function calls are semantically superfluous or not.

'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.

~~~
TheCoelacanth
A copy constructor that cannot be elided is a broken copy constructor.

~~~
ExpiredLink
This may be true for simple structs but is definitely wrong as a general
statement. Compilers cannot anticipate semantics.

~~~
TheCoelacanth
It is true for all copy constructors in C++ since the standard allows
compilers to perform copy elision.

