Return Value Optimization (RVO)
In C++ computer programming, copy elision refers to a compiler optimization technique that eliminates unnecessary copying of objects.
…
Return value optimization (RVO) is a compiler optimization that involves eliminating the temporary object created to hold a function’s return value.from Wikipedia
Copy Elision is part of the C++ since C++ 98 but it has been promoted as mandatory feature since C++ 14. Let’s consider the following code:
#include <iostream>
class Foo {
public:
Foo() {
std::cout << "ctor called!" << std::endl;
}
~Foo() {
std::cout << "dtor called!" << std::endl;
}
Foo(const Foo& other) {
std::cout << "copy ctor called!" << std::endl;
}
Foo(Foo&& other) {
std::cout << "move ctor called!" << std::endl;
}
};
There are a couple cases where the compiler can omit the call to copy/move constructors, even if they have side effects: Return Value Optimization and Passing a Temporary by Value.
Return Value Optimization
When a function return a object by value, under certain conditions the compiler is allowed to apply an optimization that involves elimintating the temporary object create to hold the return value. This is Return Value Optimization or in short RVO.
The regular case of RVO simply return a temporary value:
Foo funcRVO() {
return Foo();
}
int main() {
std::cout << "------ RVO ------" << std::endl;
Foo foo1 = funcRVO();
std::cout << "-----------------" << std::endl;
return 0;
}
When compiled and executed, the code above produces the followed output:
// clang++ foo.cpp -std=c++11 && ./a.out
------ RVO ------
ctor called!
-----------------
Some compiler offers the ability to turn off the copy elision optimizations like for example clang with the option -fno-elide-constructors
:
// clang++ foo.cpp -std=c++11 -fno-elide-constructors && ./a.out
------ RVO ------
ctor called!
move ctor called!
dtor called!
move ctor called!
dtor called!
-----------------
Looking at the two different output produced, it’s clear as the use of RVO allowed to avoid the call of quite few constructors and destructors.
This is an important optimization when copying/moving an object involves expensive operations.
There is a specialization of RVO called Named Return Value Optimization, in short NRVO: when a function return a named object as shown in the code below:
Foo funcNRVO() {
Foo foo;
return foo;
}
int main() {
std::cout << "------ NRVO ------" << std::endl;
Foo foo1 = funcNRVO();
std::cout << "-----------------" << std::endl;
return 0;
}
This code produces exactly the same output as the one for the RVO case.
When RVO cannot happen
RVO is an optimization that the compiler is allowed to apply, though there are cases when can’t be applied. Let’s look at a few examples:
Conditional return value
When the compiler can’t know from within the function which instance will be returned, it must disable RVO:
Foo func(bool condition) {
Foo a, b;
if (condition) {
return a;
}
return b;
}
int main() {
std::cout << "-----------------" << std::endl;
Foo foo = func(true);
std::cout << "-----------------" << std::endl;
return 0;
}
Returning a function parameter or a global variable
If the function returns an object created outside the scope of the function, the compiler won’t be able to apply RVO:
Foo gFooInstance;
Foo funcReturnGlobal() {
return gFooInstance;
}
Foo funcReturnParam(Foo foo) {
return foo;
}
int main() {
std::cout << "-----------------" << std::endl;
Foo foo = funcReturnGlobal();
std::cout << "-----------------" << std::endl;
Foo foo = funcReturnParam(gFooInstance);
std::cout << "-----------------" << std::endl;
return 0;
}
Returning by std::move()
Calling std::move() on the return value of a function will disable RVO in almost all the cases, because it will attempt to force the move-constructor:
Foo createFoo() {
Foo foo;
return std::move(foo);
}
int main() {
std::cout << "-----------------" << std::endl;
Foo foo = createFoo();
std::cout << "-----------------" << std::endl;
return 0;
}
// clang++ foo.cpp -std=c++11 && ./a.out
-----------------
ctor called!
move ctor called!
dtor called!
-----------------
dtor called!
Returning a member variable
struct A {
Foo foo;
};
Foo getFoo() {
return A().foo;
}
int main() {
std::cout << "-----------------" << std::endl;
Foo foo = getFoo();
std::cout << "-----------------" << std::endl;
return 0;
}
// clang++ foo.cpp -std=c++11 && ./a.out
-----------------
ctor called!
move ctor called!
dtor called!
-----------------
dtor called!
Passing a Temporary by Value
A second common case of copy elision is passing a temporary by value, C++ specifications says:
[…] when a temporary class object that has not been bound to a reference would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
Let’s see an example:
void func(Foo f) {
std::cout << "func called!" << std::endl;
}
int main() {
std::cout << "-----------------" << std::endl;
func(Foo());
std::cout << "-----------------" << std::endl;
return 0;
}
This code produces the following output if executed with and without copy elision:
// clang++ foo.cpp -std=c++11 -fno-elide-constructors && ./a.out
-----------------
ctor called!
move ctor called!
func called!
dtor called!
dtor called!
-----------------
// clang++ foo.cpp -std=c++11 && ./a.out
-----------------
ctor called!
func called!
dtor called!
-----------------