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).
std::chrono
has some of these types). Or maybe return-in-register is still ok if the type is trivially-copyable, so that determining whether elision occurred is impossible? – Cogitable