Return value optimization (RVO) is an optimization technique involving copy elision, which eliminates the temporary object created to hold a function's return value in certain situations. I understand the benefit of RVO in general, but I have a couple of questions.
The standard says the following about it in §12.8, paragraph 32 of this working draft (emphasis mine).
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.
It then lists a number of criteria when the implementation may perform this optimization.
I have a couple of questions regarding this potential optimization:
I am used to optimizations being constrained such that they cannot change observable behaviour. This restriction does not seem to apply to RVO. Do I ever need to worry about the side effects mentioned in the standard? Do corner cases exist where this might cause trouble?
What do I as a programmer need to do (or not do) to allow this optimization to be performed? For example, does the following prohibit the use of copy elision (due to the move
):
std::vector<double> foo(int bar){ std::vector<double> quux(bar,0); return std::move(quux); }
I posted this as a new question because the specific questions I mentioned are not directly answered in other, related questions.
In the context of the C++ programming language, return value optimization (RVO) is a compiler optimization that involves eliminating the temporary object created to hold a function's return value. RVO is allowed to change the observable behaviour of the resulting program by the C++ standard.
Compilers often perform Named Return Value Optimization (NRVO) in such cases, but it is not guaranteed.
(Named) Return value optimization is a common form of copy elision. It refers to the situation where an object returned by value from a method has its copy elided. The example set forth in the standard illustrates named return value optimization, since the object is named.
the NRVO (Named Return Value Optimization)
I am used to optimizations being constrained such that they cannot change observable behaviour.
This is correct. As a general rule -- known as the as-if rule -- compilers can change code if the change is not observable.
This restriction does not seem to apply to RVO.
Yes. The clause quoted in the OP gives an exception to the as-if rule and allows copy construction to be omitted, even when it has side effects. Notice that the RVO is just one case of copy-elision (the first bullet point in C++11 12.8/31).
Do I ever need to worry about the side effects mentioned in the standard?
If the copy constructor has side effects such that copy elision when performed causes a problem, then you should reconsider the design. If this is not your code, you should probably consider a better alternative.
What do I as a programmer need to do (or not do) to allow this optimization to be performed?
Basically, if possible, return a local variable (or temporary) with the same cv unqualified type as the function return type. This allows RVO but doens't enforce it (the compiler might not perform RVO).
For example, does the following prohibit the use of copy elision (due to the move):
// notice that I fixed the OP's example by adding <double> std::vector<double> foo(int bar){ std::vector<double> quux(bar, 0); return std::move(quux); }
Yes, it does because you're not returning the name of a local variable. This
std::vector<double> foo(int bar){ std::vector<double> quux(bar,0); return quux; }
allows RVO. One might be worried that if RVO is not performed then moving is better than coping (which would explain the use of std::move
above). Don't worry about that. All major compilers will do the RVO here (at least in release build). Even if a compiler doesn't do RVO but the conditions for RVO are met then it will try to do a move rather than a copy. In summary, using std::move
above will certainly make a move. Not using it will likely neither copy nor move anything and, in the worst (unlikely) case, will move.
(Update: As haohaolee's pointed out (see comments), the following paragraphs are not correct. However, I leave them here because they suggest an idea that might work for classes that don't have a constructor taking a std::initializer_list
(see the reference at the bottom). For std::vector
, haohaolee found a workaround.)
In this example you can force the RVO (strict speaking this is no longer RVO but let's keep calling this way for simplicity) by returning a braced-init-list from which the return type can be created:
std::vector<double> foo(int bar){ return {bar, 0}; // <-- This doesn't work. Next line shows a workaround: // return {bar, 0.0, std::vector<double>::allocator_type{}}; }
See this post and R. Martinho Fernandes's brilliant answer.
Be carefull! Have the return type been std::vector<int>
the last code above would have a different behavior from the original. (This is another story.)
I highly recommend reading "Inside the C++ Object Model" by Stanely B. Lippman for detailed information and some historical backround on how the named return value optimization works.
For example, in chapter 2.1 he has this to say about named return value optimization:
In a function such as bar(), where all return statements return the same named value, it is possible for the compiler itself to optimize the function by substituting the result argument for the named return value. For example, given the original definition of bar():
X bar() { X xx; // ... process xx return xx; }
__result is substituted for xx by the compiler:
void bar( X &__result ) { // default constructor invocation // Pseudo C++ Code __result.X::X(); // ... process in __result directly return; }
(....)
Although the NRV optimization provides significant performance improvement, there are several criticisms of this approach. One is that because the optimization is done silently by the compiler, whether it was actually performed is not always clear (particularly since few compilers document the extent of its implementation or whether it is implemented at all). A second is that as the function becomes more complicated, the optimization becomes more difficult to apply. In cfront, for example, the optimization is applied only if all the named return statements occur at the top level of the function. Introduce a nested local block with a return statement, and cfront quietly turns off the optimization.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With