Understanding Return Value Optimization and Returning Temporaries - C++

Understanding return value optimization and returning temporaries - C++

In two first cases RVO optimization will take place. RVO is old feature and most compilers supports it. The last case is so called NRVO (Named RVO). That's relatively new feature of C++. Standard allows, but doesn't require implementation of NRVO (as well as RVO), but some compilers supports it.

You could read more about RVO in Item 20 of Scott Meyers book More Effective C++. 35 New Ways to Improve Your Programs and Designs.

Here is a good article about NRVO in Visual C++ 2005.

What is the magic in return value optimization on this?

Because you use C++17, which promises RVO, even you added -O0.
this maybe help

c++11 Return value optimization or move?

Use exclusively the first method:

Foo f()
{
Foo result;
mangle(result);
return result;
}

This will already allow the use of the move constructor, if one is available. In fact, a local variable can bind to an rvalue reference in a return statement precisely when copy elision is allowed.

Your second version actively prohibits copy elision. The first version is universally better.

Return value optimization and copy elision in C

RVO/NRVO are clearly allowed under the "as-if" rule in C.

In C++ you can get observable side-effects because you've overloaded the constructor, destructor, and/or assignment operator to give those side effects (e.g., print something out when one of those operations happens), but in C you don't have any ability to overload those operators, and the built-in ones have no observable side effects.

Without overloading them, you get no observable side-effects from copy elision, and therefore nothing to stop a compiler from doing it.

What are copy elision and return value optimization?

Introduction

For a technical overview - skip to this answer.

For common cases where copy elision occurs - skip to this answer.

Copy elision is an optimization implemented by most compilers to prevent extra (potentially expensive) copies in certain situations. It makes returning by value or pass-by-value feasible in practice (restrictions apply).

It's the only form of optimization that elides (ha!) the as-if rule - copy elision can be applied even if copying/moving the object has side-effects.

The following example taken from Wikipedia:

struct C {
C() {}
C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
return C();
}

int main() {
std::cout << "Hello World!\n";
C obj = f();
}

Depending on the compiler & settings, the following outputs are all valid:

Hello World!

A copy was made.

A copy was made.



Hello World!

A copy was made.



Hello World!

This also means fewer objects can be created, so you also can't rely on a specific number of destructors being called. You shouldn't have critical logic inside copy/move-constructors or destructors, as you can't rely on them being called.

If a call to a copy or move constructor is elided, that constructor must still exist and must be accessible. This ensures that copy elision does not allow copying objects which are not normally copyable, e.g. because they have a private or deleted copy/move constructor.

C++17: As of C++17, Copy Elision is guaranteed when an object is returned directly:

struct C {
C() {}
C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
return C(); //Definitely performs copy elision
}
C g() {
C c;
return c; //Maybe performs copy elision
}

int main() {
std::cout << "Hello World!\n";
C obj = f(); //Copy constructor isn't called
}

Return value optimizations and side-effects

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

How does the caller of a function know whether Return Value Optimization was used?

There's no change in the interface. In all cases, the results
of the function must appear in the scope of the caller;
typically, the compiler uses a hidden pointer. The only
difference is that when RVO is used, as in your first case, the
compiler will "merge" x and this return value, constructing
x at the address given by the pointer; when it is not used,
the compiler will generate a call to the copy constructor in the
return statement, to copy whatever into this return value.

I might add that your second example is not very close to what
happens. At the call site, you get almost always something
like:

<raw memory for string> s;
f( &s );

And the called function will either construct a local variable
or temporary directly at the address it was passed, or copy
construct some othe value at this address. So that in your last
example, the return statement would be more or less the
equivalent of:

if ( cont ) {
std::string::string( s, first );
} else {
std::string::string( s, second );
}

(Showing the implicit this pointer passed to the copy
constructor.) In the first case, if RVO applies, the special
code would be in the constructor of x:

std::string::string( s, "hi" );

and then replacing x with *s everywhere else in the function
(and doing nothing at the return).

Interaction between std::move, return value optimization and destructors

What will happen there? Is this guaranteed to work? Or can there be a situation in which the code doesn't compile or, worse, the destructor gets called twice on the same object?

As long as you're using automatic variables and temporary objects, C++ guarantees that each object constructed will be destroyed. Elisions will not break the meaning of your code; it's just an optimization.

Copy elision doesn't really elide a copy/move; it elides the existence of an object.

In pre-C++17, your code says to construct an ObjectContainer temporary, then copy/move it into the ObjectContainer return value from the function. A compiler may elide the creation of the temporary, instead constructing the return value object directly from the temporary's given parameters. In doing so, it elides away the copy/move, but it also elides away the destruction of the temporary that it doesn't create.

(in post-C++17, this code says to use the prvalue to initialize the return value object directly, so there's no possibility of a temporary being used here.)

Do I have to specify a copy constructor?

You only need to specify a copy constructor if you want the object to be copyable. If you provide a move constructor to a type, the compiler will automatically = delete the other copy/move constructors/assignment operators unless you explicitly write them.

What happens if I do?

That all depends on what you put into the copy constructor. If you do a standard copy of the value, then you've broken how your type works, since it's supposed to have unique ownership of the allocated pointer.

Or should I use std::move in the return statement?

... why would you? The return statement is given a prvalue; you never need to use std::move on prvalues.



Related Topics



Leave a reply



Submit