Copy and move elision is an optimisation technique used by many C++ compilers to avoid unnecessary copying and moving of temporary variables.
This speeds up processes that may otherwise be slow and inefficient, especially with objects that are “expensive” — such as containers or complex user-defined types.
Let’s take a look at an example to understand what this optimisation does.
int x = 0;
// default ctor
std::cout << "Default ctor\n";
// copy ctor
Foo(const Foo& rhs)
std::cout << "Copy ctor\n";
temp.x = 42; // update member variable
And for all the nerds out there, I will be using g++ 11.4.0 for this demo.
$ g++ --version
g++ (GCC) 11.4.0
If we compile the code above and run the executable, we get the following output (note that we are compiling in C++14, more on this later).
$ g++ -std=gnu++14 copyelision.cpp -o app
Now, if we compile the same code, but this time we explicitly telling our compiler to not elide constructors using
-fno-elide-constructors, we will get the following output.
$ g++ -std=gnu++14 -fno-elide-constructors copyelision.cpp -o appnoelide
Woah! That is a lot more output than when we compiled it with default settings! Let’s take a look at why this is the case.
When we declare
t1 and initialised it with the returned rvalue from
CreateFooA(). The following will happen:
CreateFooA()creates a temporary
Fooobject to return.
- The temporary object will then be copied into the object that will be returned by
- The value returned by
CreateFooA()will then be copied into
This is tedious and would adversely affect performance if the object we are dealing with is expensive to copy!
The initialisation of
t2 goes through a similar process:
CreateFooB()creates a temporary
- Do things with
temp. In our case, we update the member variable
tempthen has to be copied into the value to be returned by
- The value returned by
CreateFooB()will then be copied into
Again, very tedious and inefficient. In short, the whole process can be summarised as the following.
Default ctor # construct temporary variable to return in CreateFooA()
Copy ctor # copying temporary variable to returning object of CreateFooA()
Copy ctor # copying rvalue returned by CreateFooA() into t1
Default ctor # construct 'temp' inside CreateFooB()
Copy ctor # copy 'temp' into to returning object of CreateFooB()
Copy ctor # copying rvalue returned by CreateFooB() into t2
C++11’s move semantics help improve this by avoiding copying resources, but moving them instead.
// Move constructor
std::cout << "Move ctor\n";
By adding a move constructor, our output without eliding constructors will now be as follows.
CreateFooB() returns a named variable, which means it is an lvalue. Why is it able to call on the move constructor?
This is because of a rule in the C++ standard, specifically in 12.8. It states that when a function has a class return type and criteria for copy elision is met, if the expression to be returned is a named variable (lvalue), the object is treated as an rvalue when selecting the constructor for the copy i.e. move constructor will be used if it is available.
This makes uncopyable objects like
std::unique_ptr able to be returned by value even if it is a named variable.
auto ptr = std::make_unique<int>(0);
return ptr; // This compiles!
It is important to note though, that if a copy or move constructor is elided, that constructor must still exist — This ensures that the optimisation still respects whether an object is of a class that is uncopyable or unmovable.
Now, if we go back to the output of the executable with copy elision optimisation enabled — remember that it is enabled by default!
Here we elide extra calls to constructors, and directly construct the “to be returned” objects to the variable it is assigned to.
Comparing the volume of output, this optimisation technique improves speed and efficiency. The optimisations performed on
CreateFooB() are officially called RVO (Return Value Optimisation) and N(Named)RVO respectively.
Since this optimisation is enabled by default, it is crucial to remember that this elision will be applied even if it means not calling any code that might be present in the constructors. Therefore, critical logic inside copy/move constructors should be avoided as we cannot rely on them being called!
C++17’s Guarantee on RVO
As a last note of this post, remember how we compiled the code earlier in C++14? This is because C++17 provides us with a guarantee for copy elision for RVO. However this is not the case for NRVO.
This means that
CreateFooA() is guaranteed to elide copying/moving, whilst
CreateFooB() might or might not elide them.
$ g++ -std=gnu++17 -fno-elide-constructors copyelision.cpp -o appnoelide
$ g++ -std=gnu++17 copyelision.cpp -o app
# Similar output to C++14
# Even if we disable copy/move elision, it will still be performed for
# RVO situations
Hopefully that clears things on copy/move elision and RVO/NRVO in C++. Feel free to leave a comment if there are any doubts, or something you would like to add!