What Are Copy Elision and Return Value Optimization

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

copy elision in c++03

Yes, copy ellision is permitted in C++03 and C++98. That's the paragraph for C++98 and C++03:

Non-mandatory elision of copy operations

Under the following circumstances, the compilers are permitted, but
not required to omit the copy construction of
class objects even if the copy constructor and the
destructor have observable side-effects. The objects are constructed
directly into the storage where they would otherwise be copied
to. This is an optimization: even when it takes place and the
copy constructor is not called, it still must be
present and accessible (as if no optimization happened at all),
otherwise the program is ill-formed:

  • In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a
    function parameter or a catch clause parameter, and which is of the
    same class type (ignoring cv-qualification) as the function return
    type. This variant of copy elision is known as NRVO, "named return
    value optimization".

  • In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring
    cv-qualification) as the target object. When the nameless temporary is
    the operand of a return statement, this variant of copy elision is
    known as RVO, "return value optimization".

When copy elision occurs, the implementation treats the source and target of the omitted copy 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

cppreference

I removed everything that's only valid since C++11.

The only differences between C++98, C++03 and C++11 regarding ellision are move operations and exception handling.

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

Can I rely on named return value optimisation for complicated return types?

Before C++17, you cannot rely on copy elision at all, since it is optional. However, all mainstream compilers will very likely apply it (e.g., GCC applies it even with -O0 optimization flag, you need to explicitly disable copy elision by -fno-elide-constructors if you want to).

However, std::set supports move semantics, so even without NRVO, your code would be fine.

Note that in C++17, NRVO is optional as well. RVO is mandatory.


To be technically correct, IMO, there is no RVO in C++17, since when prvalue is returned, no temporary is materialized to be moved/copied from. The rules are kind of different, but the effect is more or less the same. Or, even stronger, since there is no need for copy/move constructor to return prvalue by value in C++17:

#include <atomic>

std::atomic<int> f() {
return std::atomic<int>{0};
}

int main() {
std::atomic<int> i = f();
}

In C++14, this code does not compile.

Why does Return Value Optimization not happen if no destructor is defined?

The language rule which allows this in case of returning a prvalue (the second example) is:

[class.temporary]

When an object of class type X is passed to or returned from a function, if X has at least one eligible copy or move constructor ([special]), each such constructor is trivial, and the destructor of X is either trivial or deleted, implementations are permitted to create a temporary object to hold the function parameter or result object.
The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the eligible trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).
[Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers.
— end note
]



Why does Return Value Optimization not happen [in some cases]?

The motivation for the rule is explained in the note of the quoted rule. Essentially, RVO is sometimes less efficient than no RVO.

If a destructor is defined by enabling the #if above, then the RVO does happen (and it also happens in some other cases such as defining a virtual method or adding a std::string member).

In the second case, this is explained by the rule because creating the temporary is only allowed when the destructor is trivial.

In the NRVO case, I suppose this is up to the language implementation.

Copy elision and return value optimization versus copy constructor

Yes. Copy elision can change the behavior of your code if your copy constructor (or your move constructor or your destructor) has side effects.

That's the whole point. If it could not change the behavior, there would be no reason to mention it in the standard. Optimizations which don't change behavior are already covered by the as-if rule. (1.9/1) That is:

The semantic descriptions in this International Standard define a
parameterized nondeterministic abstract machine. This International
Standard places no requirement on the structure of conforming
implementations. In particular, they need not copy or emulate the
structure of the abstract machine. Rather, conforming implementations
are required to emulate (only) the observable behavior of the abstract
machine
as explained below.

Copy elision is explicitly mentioned in the standard precisely because it potentially violates this rule.

Why doesn't RVO happen with structured bindings when returning a pair from a function using std::make_pair?

std::make_pair is a function that takes the arguments by reference. Therefore temporaries are created from the two Test() arguments and std::make_pair constructs a std::pair from these, which requires copy-constructing the pair elements from the arguments. (Move-constructing is impossible since your manual definition of the copy constructor inhibits the implicit move constructor.)

This has nothing to do with structured bindings or RVO or anything else besides std::make_pair.

Because std::pair is not an aggregate class, you cannot solve this by simply constructing the std::pair directly from the two arguments either. In order to have a std::pair construct the elements in-place from an argument list you need to use its std::piecewise_construct overload:

auto func() {
return std::pair<Test, Test>(std::piecewise_construct, std::forward_as_tuple(), std::forward_as_tuple());
}

understanding copy constructor calls and named return value optimization

UPDATE: addressing the output of the updated program, using return rvo rather than return (rvo);

I am in constructor
I am in constructor
I am in destructor
I am in destructor

The reason you see this is that both objects (MyMethod::rvo and main::rvo) undergo default construction, then the latter is assigned to as a separate action but you're not logging that.


You can get a much better sense of what is going on by outputting the addresses of the objects, and the this pointer values as functions are called:

#include <cstdio>
#include <iostream>
class RVO
{
public:
RVO(){
printf("%p constructor\n", this); }
RVO(const RVO& c_RVO) {
printf("%p copy constructor, rhs %p\n", this, &c_RVO); }
~RVO(){
printf("%p destructor\n", this); }
int mem_var;
};
RVO MyMethod(int i)
{
RVO rvo;
std::cout << "MyMethod::rvo @ " << &rvo << '\n';
rvo.mem_var = i;
return (rvo);
}
int main()
{
RVO rvo=MyMethod(5);
std::cout << "main::rvo @ " << &rvo << '\n';
}

The output will also depend on whether you compile with optimisations; you link to Microsoft documentation, so perhaps you're using the Microsoft compiler - try cl /O2.

Why is temporary not destroyed in Mymethod in the second version?

There was no temporary there - the object in main was directly copy-constructed. Stepping you through it:

002AFA4C constructor
MyMethod::rvo @ 002AFA4C // MyMethod::rvo's constructed

002AFA70 copy constructor, rhs 002AFA4C // above is copied to 2AFA70
002AFA4C destructor // MyMethod::rvo's destructed
main::rvo @ 002AFA70 // turns out the copy above was directly to main::rvo
002AFA70 destructor // main::rvo's destruction

[Alf's comment below] "directly copy-constructed" is not entirely meaningful to me. I think the OP means the rvo local variable

Consider the enhanced output from the first version of the program (without optimisation):

002FF890 constructor  // we find out this is main::rvo below
002FF864 constructor // this one's MyMethod::rvo
MyMethod::rvo @ 002FF864
002FF888 copy constructor, rhs 002FF864 // 2FF888 is some temporary
002FF864 destructor // there goes MyMethod::rvo
002FF888 destructor // there goes the temporary
main::rvo @ 002FF890
002FF890 destructor // and finally main::rvo

If we tie that back in to the OP's output and annotations...

I am in constructor       // main rvo construction
I am in constructor //MyMethod rvo construction
I am in copy constructor //temporary created inside MyMethod
I am in destructor //Destroying rvo in MyMethod
I am in destructor //Destroying temporary in MyMethod
I am in destructor //Destroying rvo of main

The OP (correctly) refers to the copy-constructed object as a temporary. When I say of the second version of the program "There was no temporary there - the object in main was directly copy-constructed." - I mean that there's no temporary equivalent to that in the first program we analysed directly above, and instead it's main::rvo that's copy-constructed from MyMethod::rvo.

Why return value optimization does not work when return ()

This is NRVO, not RVO.

Here is the rule which allows NRVO (class.copy/31):

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv- unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

As you can see, in the case of (a), the expression is not a name (because of the added parenthesis), so NRVO is not allowed.



Related Topics



Leave a reply



Submit