I'm trying to understand lifetime guaranteed by the C++17 Standard, in particular for guaranteed copy elisions. Let's start with an example
std::string make_tmp();
std::string foo() {
return std::string{make_tmp().c_str()};
}
My undestanding of what's happening:
make_tmp
creates a temporary string
we will call t
; foo
returns a (needlessly created) temporary (copy of t
's c_str
).
The standard (even pre C++17) guarantees the lifetime of t
to be the time until the full return expressions has been evaluated.
Thus it is safe so create the temporary copy of t
(to be returned).
Now copy elisions kicks in; more specifically, the second bullet in the first C++17 block:
In a function call, if the operand of a return statement is a prvalue and the return type of the function is the same as the type of that prvalue.
Consequently the temporary copy will not even be created at all.
Follow-up Questions:
Does the returned temporary copy still imply a sufficiently extended lifetime of t
-- even though it is guaranteed to be elided?
Consider the variant of foo
given below.
I'm assuming, copy elision is no longer required (but rather highly likely).
If the copy will not be elided, the standard's got us covered (by the arguments above).
In case the copy is elided, does the standard still guarantee a sufficient lifetime of t
despite the type of the return
ed-expression being different from foo
's return type?
foo
-Variant:
std::string foo() {
return make_tmp().c_str();
}
I'd like to understand the guarantees purely implied by the standard.
Please note, that I'm aware of the fact that both foo
versions "work" (ie. there are no dangling pointers involved even when testing with custom classes under various compilers).
Copy elision (NRVO) is allowed there and is routinely performed by most compilers, but is still non-guaranteed, and the widget class cannot be non-copyable non-movable.
Copy elision is an optimization implemented by most compilers to prevent extra (potentially expensive) copies in certain situations. It makes returning by value or pass-by-value feasible in practice (restrictions apply).
This particular technique was later coined "Named return value optimization" (NRVO), referring to the fact that the copying of a named object is elided.
I think there's some confusion here as to which copies are being elided. Let's take the furthest out view:
std::string make_tmp();
std::string foo() {
return std::string{make_tmp().c_str()};
}
std::string s = foo();
Here, potentially there are four std::string
s created: make_tmp()
, the temporary std::string{...}
constructed from it, the return object of foo()
, and s
. Which implies three copies (I'm just going to use the word copy for consistency, even if all of these are moves. Hopefully this won't be confusing).
Copy elision allows for the removal of two of these copies:
std::string{...}
to the return object of foo()
. foo()
to s
Both of these elisions are mandated in C++17's "guaranteed copy elision" - because we're initializing from a prvalue (a term that's a little confusing in that we're not actually performing overload resolution to determine we need to perform a copy construction and then skipping it, we're just directly initializing). The code is identical to:
std::string s{make_tmp().c_str()};
This cannot be removed though - we're still constructing a string
via make_tmp()
, pulling out its contents, and then constructing a new string
from them. No way around that.
The provided variant has exactly the same behavior.
This answer directly answers the lifetime issues asked in OP (and you can see it has nothing to do with copy elision). If you are not familiar with the whole story happened during the execution of the return statement, you can refer to Barry's answer.
Yes, the temporary is guaranteed to persist during the copy-initialization of the returned object per [stmt.return]/2:
The copy-initialization of the result of the call 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 ([stmt.jump]) of the block enclosing the return statement.
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