Return value optimization (copy elision)

Now that most code bases are using modern (post C++11) compilers, it’s common to encounter the move semantic judiciously used throughout. Implicitly or explicitly. Which brings us to return value optimization (RVO), which the standard refers to as copy elision [Copying and moving class objects / 12.8.31].

“ … 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.”

In brief, RVO is an optimization (compiler isn’t required to, but the fallback isn’t bad either) where the return value is constructed directly into the returned value location. Meaning, you avoid all copying. I say all, since no conversion can take place since for the RVO to considered by the compiler, the return type and the returned to object must be the same type. Basically, this:

A passBackAnotherA()
{

   // Some more code.

   return A( /* Some arguments determined above */ );
}

// or 

A passBackAnA()
{
   A a;

   // Some other code.

   // This is referred to as Name RVO / NRVO. 
   return a;
}

Which brings us to an ill-advised use of move.

A movingVersion()
{
   A a;

   // Some code.

   return move(a);
}

A aVal = movingVersion();

In the move version above, we’ve stopped the compiler from performing RVO as mention in [Copying and moving class objects / 12.8.31.3]. Meaning we’re incurring the unnecessary cost of a move instead of constructing in place directly into the returned to variable aVal.

Some notes to keep in mind:

1. Side-effects: The compiler is able to perform this optimization, even if it has side effects: “…even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects”. Something to think about if your constructor/destructor does logging etc you were expecting.

2. Fallback: If the compile can’t elide for some reason, it must treat it as if there was an implicit move on the return value, and the move constructor will come into play. [Copying and moving class objects / 12.8.32]

3. Accessibility: Since the constructor or move constructor could come into play, the standard stipulates the constructors must be accessible, even if they are optimized away (elided). “This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided.”

Final thoughts:
This is probably the closest one gets in terms of C++ where “overthinking” and applying a move could lead to slower code. I say close. Since a bit of extra thought will keep the code base lean and mean.

Leave a Reply

Your email address will not be published. Required fields are marked *