C++ Move Semantics Notes
Page Contents
Return Value Optimisation
RVO is a compiler strategy that helps avoid copying objects returned by value. It comes in two flavours, unnamed RVO (RVO) and named RVO (NRVO). The former concerns the return of temporary objects, i.e., objects that have no name and you can't take the address of. The latter concers the return of named objects who's lifespan ends with the function return.
For example, return MyObj();
, returns a temporary object (RVO) and MyObj a; return a
returns a named object (NRVO).
It iss the only form of optimization that bypasses the as-if rule - copy elision can be applied even if copying/moving the object has side-effects.
Take the following example:
// Use `g++ demo_no_move_sem.cpp -O0 -fno-elide-constructors && ./a.out`
// `-fno-elide-constructors` disable return value optimization.
#include <cstring>
#include <iostream>
#include <iterator>
struct MyObj {
MyObj() {
std::cout << "Constuctor\n";
}
~MyObj() {
std::cout << "Destructor\n";
}
MyObj(const MyObj& other) {
std::cout << "Copy constructor\n";
}
};
MyObj CreateObj_URVO() {
return MyObj();
}
MyObj CreateObj_NRVO() {
MyObj a;
return a;
}
int main(void) {
std::cout << "URVO\n";
MyObj r1 = CreateObj_URVO();
std::cout << "\n\nNRVO\n";
MyObj r2 = CreateObj_NRVO();
std::cout << "\n\nEND\n";
}
If you run it with and without the -fno-elide-constructors
it gives a clue as to what RVO and copy elision is...
no-elide-constructors | elide-constructors |
---|---|
URVO Constuctor NRVO Constuctor Copy constructor Destructor END Destructor Destructor |
URVO Constuctor NRVO Constuctor END Destructor Destructor |
When constructor elision is disabled using the -fno-elide-constructors
GCC option NRVO is disabled:
NRVO
Constuctor -- 1. `MyObj a;` in `CreateObj_NRVO() on stack`.
Copy constructor -- 2. `CreateObj_NRVO()` returns. `MyObj a` copied into `r1`.
Destructor -- 3. `MyObj a;` destroyed as function exit complete.
When constructor elision is permitted the following is seen:
NRVO
Constructor -- 1. `MyObj a;` in `CreateObj_NRVO()` but uses the memory allocated to `r1`.
Therefore on return, `r1` is already valid so no copy-out-of-function
is required! Saves a copy and destruction!
Interestingly the URVO is not affected... it is always used. The reason is explained here:
Return-value optimization is part of a category of optimizations enabled by "copy elision" (meaning "omitting copying"). C++17 requires copy elision when a function returns a temporary object (unnamed object), but does not require it when a function returns a named object.
One important thing to note is that, as explained in the article referenced above,
the compiler generates code such that object copying is avoided if the return value is used as the initializer for a receiving variable
.
Thus, doing something like this, stops either type of RVO:
MyObj r1; // Default constructor used
r1 = CreateObj_URVO(); // No RVO possible.