How does guaranteed copy elision work?

Solution 1:

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 redefines a number of C++ concepts, such that certain circumstances where copies/moves could be elided don't actually provoke a copy/move at all. The compiler isn't eliding a copy; the standard says that no such copying could ever happen.

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.

Similarly:

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 redefines the meaning of a prvalue expression. Pre-C++17, prvalues are temporary objects. In C++17, a prvalue expression is merely something which can materialize a temporary, but it isn't a temporary yet.

If you use a prvalue to initialize an object of the prvalue's type, then no temporary is materialized. When you do return T();, this initializes the return value of the function via a prvalue. Since that function returns T, no temporary is created; the initialization of the prvalue simply directly initilaizes the return value.

The thing to understand is that, since the return value is a prvalue, it is not an object yet. It is merely an initializer for an object, just like T() is.

When you do T t = Func();, the prvalue of the return value directly initializes the object t; there is no "create a temporary and copy/move" stage. Since Func()'s return value is a prvalue equivalent to T(), t is directly initialized by T(), exactly as if you had done T t = T().

If a prvalue is used in any other way, the prvalue will materialize a temporary object, which will be used in that expression (or discarded if there is no expression). So if you did const T &rt = Func();, the prvalue would materialize a temporary (using T() as the initializer), whose reference would be stored in rt, along with the usual temporary lifetime extension stuff.

One thing guaranteed elision 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.

Guaranteed elision also works with direct initialization:

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 the allocated 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 these function internally follow the rules of guaranteed copy elision, of course. They have to 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).

Solution 2:

I think details of copy elision have been well shared here. However, I found this article: https://jonasdevlieghere.com/guaranteed-copy-elision which refers to guaranteed copy elision in C++17 in return value optimization case.

It also refers to how using the gcc option: -fno-elide-constructors, one can disable the copy elision and see that instead of the constructor directly being called at destination, we see 2 copy constructors(or move constructors in c++11) and their corresponding destructors being called. Following example shows both cases:

#include <iostream>
using namespace std;
class Foo {
public:
    Foo() {cout << "Foo constructed" << endl; }
    Foo(const Foo& foo) {cout << "Foo copy constructed" << endl;}
    Foo(const Foo&& foo) {cout << "Foo move constructed" << endl;}
    ~Foo() {cout << "Foo destructed" << endl;}
};

Foo fReturnValueOptimization() {
    cout << "Running: fReturnValueOptimization" << endl;
    return Foo();
}

Foo fNamedReturnValueOptimization() {
    cout << "Running: fNamedReturnValueOptimization" << endl;
    Foo foo;
    return foo;
}

int main() {
    Foo foo1 = fReturnValueOptimization();
    Foo foo2 = fNamedReturnValueOptimization();
}
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 testFooCopyElision.cxx # Copy elision enabled by default
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo destructed
Foo destructed
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 -fno-elide-constructors testFooCopyElision.cxx # Copy elision disabled
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Foo destructed
Foo destructed

I see that return value optimization .i.e. copy elision of temporary objects in return statements generally being guaranteed irrespective of c++ 17.

However, named return value optimization of returned local variables happening mostly but not guaranteed. In a function with different return statements, I see that if the each of the return statements returns variables of local scope, or variables of same scope it will happen. Otherwise, if in different return statements variables of different scopes are returned it would be hard for compiler to perform copy elision.

It would be nice, if there was a way to guarantee copy elision or get some sort of warning when copy elision can't be performed which would make developers make sure copy elision is performed and re-factor code if it couldn't be performed.