简体   繁体   中英

Copy Elision for Returned Temporaries

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:

  1. Does the returned temporary copy still imply a sufficiently extended lifetime of t -- even though it is guaranteed to be elided?

  2. 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).

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:

  • The copy from std::string{...} to the return object of foo() .
  • The copy from 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.

Does the returned temporary copy still imply a sufficiently extended lifetime of t -- even though it is guaranteed to be elided?

t will be on the foo 's body, and the elision happens in the make_tmp 's body. So the t 's lifetime is not affected by elision is the foo 's body in any way, being it temporary, static, dynamic or whatever.

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 returned-expression being different from foo's return type?

make_tmp().c_str() is equivalent to std::string(make_tmp().c_str()) in your original snippet, the std::string constructor call happens implicitly. As you mentioned in the beginning of your post, the elision does happen.

I think to understand the guarantees of the elision it is much better to follow the understanding of how the return logic works on the assembly level. This will give you the understanding how the compiler makes the return mechanism of the call, the standard here is just trying to keep up with the actual compiler implementation, giving the clarity, rather introducing some new language syntax concept.

Simple example:

std::string foo();
int main() {
  auto t = foo();
}

In assembly the relevant part the main body will look like this:

0000000000400987 <main>:
....
  ; Allocate 32-byte space (the size of `std::string` on x64) on the stack
  ; for the return value
  40098b:   48 83 ec 20             sub    $0x20,%rsp
  ; Put the pointer of the stack allocated chunk to RAX
  40098f:   48 8d 45 e0             lea    -0x20(%rbp),%rax
  ; Move the pointer from RAX to RDI
  ; RDI - is a first argument location for a callee by the calling convention
  ; By calling convention, the return of not trivial types (`std::string` in our case)
  ; must be taken care on the caller side, it must allocate the space for the return type
  ; and give the pointer as a first argument (what of course, is hidden by the compiler
  ; for C/C++)
  400993:   48 89 c7                mov    %rax,%rdi
  ; make a call
  400996:   e8 5b ff ff ff          callq  4008f6 <foo()>
  ; At this point you have the return value at the allocated address on the main's stack
  ; at RBP - 32 location. Do whatever further.
....

What effectively happens is that the t space is already on the caller's ( main 's) stack and the address of this stack memory is passed to the callee, foo . foo only needs to put the stuff in by whatever logic in it and that is all. foo might allocate some memory to build std::string and then copy this memory to the given memory, but it also might (what is an easy optimization in many cases) just directly work on the given memory without allocating anything. In the latter, compiler might call a copy-constructor, but does not make sense. C++17 standard just was clarifying on this fact.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM