jotik jotik - 4 months ago 29
C++ Question

How does guaranteed copy elision work?

At the 2016 Oulu ISO C++ Standards meeting, a proposal called Guaranteed Copy Elision was voted into C++17 by the standards committee.

How exactly does it work? Does it cover some cases where copy elision was already permitted, or are code changes needed to guarantee copy elision?


Copy elision was permitted to happen under a number of circumstances. However, even if it was permitted, the code still had to be able to work as if the copy were not elided. Namely, there had to be an accessible copy and/or move constructor.

Guaranteed copy elision simply spells out a number of circumstances where there no longer has to be an accessible copy/move constructor. That is, the code is guaranteed to elide the copy operation.

Consider this function:

T Func() {return T();}

Under non-guaranteed copy elision rules, this will create a temporary, then move from that temporary into the function's return value. That move operation may be elided, but T must still have an accessible move constructor even if it is never used.


T t = Func();

This is copy initialization of t. This will copy initialize t with the return value of Func. However, T still has to have a move constructor, even though it will not be called.

Guaranteed copy elision says that in these two circumstances (and several others), the elision of the copy/move operation is certain. And therefore, T need not have copy/move constructors.

One thing this permits you to do is return objects which are immobile. For example, lock_guard cannot be copied or moved, so you couldn't have a function that returned it by value. But with guaranteed copy elision, you can.

So long as you follow the rules. The general idea is that a prvalue expression is not a temporary; it is an initializer for an object. If the expression that uses a prvalue is used as an initializer for an object of the same type as the prvalue, then it initializes it directly, without copying or moving. And therefore, the type does not need to have an accessible copy/move constructor.

If a prvalue is used in any other way, the prvalue will initialize a temporary, which will be used in that expression (or discarded if there is no expression).

So if you initialize the return value of a function with a temporary or the return value of some other function, then there is no copy of the return value; it simply initializes the return value object directly. If you initialize a stack variable with the return value of a function of the same type, then the stack variable is initialized directly.

You even get direct initialization with this:

new T(FactoryFunction());

If FactoryFunction returns T by value, this expression will not copy the return value into the allocated memory. It will instead allocate memory and use that memory as the return value memory for the function call directly.

So factory functions that return by value can directly initialize heap allocated memory without even knowing about it. So long as they internally follow the rules of guaranteed copy elision. That is, they return a prvalue of type T.

Of course, this works too:

new auto(FactoryFunction());

In case you don't like writing typenames.

It is important to recognize that the above guarantees only work for prvalues. That is, you get no guarantee when returning a named variable:

T Func()
   T t = ...;
   return t;

In this instance, t must still have an accessible copy/move constructor. Yes, the compiler can choose to optimize away the copy/move. But the compiler must still verify the existence of an accessible copy/move constructor.

So nothing changes for named return value optimization (NRVO).