At the 2016 Oulu ISO C++ Standards meeting, a proposal called Guaranteed copy elision through simplified value categories was voted into C++17 by the standards committee.
How exactly does guaranteed copy elision work? Does it cover some cases where copy elision was already permitted, or are code changes needed to guarantee copy elision?
You can see this behavior in compiler versions Visual Studio 2017 15.6, Clang 4, GCC 7, and above. Despite the name of the paper and what you might read on the Internet, the new rules do not guarantee copy elision. Instead, the new value category rules are defined such that no copy exists in the first place.
In a return statement or a throw-expression, if the compiler cannot perform copy elision but the conditions for copy elision are met or would be met, except that the source is a function parameter, the compiler will attempt to use the move constructor even if the object is designated by an lvalue; see return statement ...
GCC provides the -fno-elide-constructors option to disable copy-elision. This option is useful to observe (or not observe) the effects of return value optimization or other optimizations where copies are elided. It is generally not recommended to disable this important optimization.
the NRVO (Named Return Value Optimization)
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.
Despite the name of the paper and what you might read on the Internet, the new rules do not guarantee copy elision. Instead, the new value category rules are defined such that no copy exists in the first place.
The standard allows this copy to be elided by constructing the return value at the call-site rather than in make ( C++11 [class.copy]/31 ). This is called copy elision. The unfortunate part is this: even if all copies of the type are elided, the constructor still must exist. This means that if we instead have:
In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With