It seems like a silly question, but is the exact moment at which return xxx;
is \"executed\" in a function unambiguously defined?
Please see the following e
It's RVO (+ returning copy as temporary which fogs the picture), one of the optimization that are allowed to change visible behaviour:
10.9.5 Copy/move elision (emphases are mine):
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects**. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object.
This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
- in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function parameter or a variable introduced by the exception-declaration of a handler) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function call's return object
- [...]
Based on whether it's applied your whole premise gets wrong. At 1. the c'tor for res
is called, but the object might live inside of make_string_ok
or outside.
Bullets 2. and 3. might not happen at all, but this is a side point. Target got side effects of Writer
s dtor affected, was outside of make_string_ok
. This happened to be a temporary created by using the make_string_ok
in the context of evaluation operator<<(ostream, std::string)
. The compiler created a temporary value, and then executed the function. This is important because temporary lives outside of it, so the target for Writer
is not local to make_string_ok
but to operator<<
.
Meanwhile, your second example does not fit the criterion (nor the ones omitted for brevity) because the types are different. So the writer dies. It would even die, if it were a part of the pair
. So here, a copy of res.first
is returned as a temporary object, and then dtor of Writer
affects the original res.first
, which is about to die itself.
It seems pretty obvious that the copy is made before calling destructors, because the object returned by copy is also destroyed, so you'd not be able to copy it otherwise.
After all it boils down to RVO, because the d'tor of Writer
either works on the outside object or on the local one, according to whether the optimization is applied or not.
No, the optimization is optional, though it can change the observable behaviour. It's at the compiler's discretion to apply it or not. It's an exempt from the "general as-if" rule which says compiler is allowed to make any transformation which does not change observable behaviour.
A case for it became mandatory in c++17, but not yours. The mandatory one is where the return value is an unnamed temporary.
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()
.
Due to Return Value Optimization (RVO), a destructor for std::string res
in make_string_ok
may not be called. The string
object can be constructed on the caller's side and the function may only initialize the value.
The code will be equivalent to:
void make_string_ok(std::string& res){
Writer w(res);
}
int main() {
std::string res("A");
make_string_ok(res);
}
That is why the value return shall be "AB".
In the second example, RVO does not apply, and the value will be copied to the returned value exactly upon the call to return, and Writer
's destructor will run on res.first
after the copy occurred.
6.6 Jump statements
On exit from a scope (however accomplished), destructors (12.4) are called for all constructed objects with automatic storage duration (3.7.2) (named objects or temporaries) that are declared in that scope, in the reverse order of their declaration. Transfer out of a loop, out of a block, or back past an initialized variable with automatic storage duration involves the destruction of variables with automatic storage duration that are in scope at the point transferred from...
...
6.6.3 The Return Statement
The copy-initialization of the returned entity is sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the return statement, which, in turn, is sequenced before the destruction of local variables (6.6) of the block enclosing the return statement.
...
12.8 Copying and moving class objects
31 When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.(123) This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
— in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
123) Because only one object is destroyed instead of two, and one copy/move constructor is not executed, there is still one object destroyed for each one constructed.