There is a concept in C++ called elision.
Elision takes two seemingly distinct objects and merges their identity and lifetime.
Prior to c++17 elision could occur:
When you have a non-parameter variable Foo f;
in a function that returned Foo
and the return statement was a simple return f;
.
When you have an anonymous object being used to construct pretty much any other object.
In c++17 all (almost?) cases of #2 are eliminated by the new prvalue rules; elision no longer occurs, because what used to create a temporary object no longer does so. Instead, the construction of the "temporary" is directly bound to the permanent object location.
Now, elision isn't always possible given the ABI that a compiler compiles to. Two common cases where it is possible are known as Return Value Optimization and Named Return Value Optimization.
RVO is the case like this:
Foo func() {
return Foo(7);
}
Foo foo = func();
where we have a return value Foo(7)
which is elided into the value returned, which is then elided into the external variable foo
. What appears to be 3 objects (the return value of foo()
, the value on the return
line, and Foo foo
) is actually 1 at runtime.
Prior to c++17 the copy/move constructors must exist here, and the elision is optional; in c++17 due to the new prvalue rules no copy/move constructr need exist, and there is no option for the compiler, there must be 1 value here.
The other famous case is named return value optimization, NRVO. This is the (1) elision case above.
Foo func() {
Foo local;
return local;
}
Foo foo = func();
again, elision can merge the lifetime and identity of of Foo local
, the return value from func
and Foo foo
outside of func
.
Even c++17, the second merge (between func
's return value and Foo foo
) is non-optional (and technically the prvalue returned from func
is never an object, just an expression, which is then bound to construct Foo foo
), but the first remains optional, and requires a move or copy constructor to exist.
Elision is a rule that can occur even if eliminating those copies, destructions and constructions would have observable side effects; it is not an "as-if" optimization. Instead, it is subtle change away from what a naive person might think C++ code means. Calling it an "optimization" is more than a bit of a misnomer.
The fact it is optional, and that subtle things can break it, is an issue with it.
Foo func(bool b) {
Foo long_lived;
long_lived.futz();
if (b)
{
Foo short_lived;
return short_lived;
}
return long_lived;
}
in the above case, while it is legal for a compiler to elide both Foo long_lived
and Foo short_lived
, implementation issues make it basically impossible, as both objects cannot both have their lifetimes merged with the return value of func
; eliding short_lived
and long_lived
together is not legal, and their lifetimes overlap.
You can still do it under as-if, but only if you can examine and understand all side effects of destructors, constructors and .futz()
.
return
happens, until C++14(!) the wording forreturn
didn't say that local temporaries lasted long enough to be used in constructing the return value. – Loriannereturn std::move(pointer)
, but alsoreturn pointer;
and it works without move semantic active, Copy elision is in some way an optimization of move semantics on its own, when you don't really need a temporal object. It never existed, so there was no extra resource acquisition, so there is nothing to destroy. Only case when it hurts RAII is when "resource acquisition" treated as "interface ownership", which was a point of argument for a while. – Causalgia*this
? Of course dtors are all about side effects. – Ostmark