返回临时人员的复制省略
Copy Elision for Returned Temporaries
我试图了解 C++17 标准保证的生命周期,特别是 保证 复制省略。
让我们从一个例子开始
std::string make_tmp();
std::string foo() {
return std::string{make_tmp().c_str()};
}
我对正在发生的事情的理解:
make_tmp
创建一个临时的 string
我们将调用 t
; foo
returns 一个(不必要地创建的)临时文件(t
的 c_str
的副本)。
标准(甚至是 C++17 之前的版本)保证 t
的生命周期是整个 return 表达式被计算的时间。
因此创建 t
的临时副本是安全的(returned)。
现在 copy elisions 开始;更具体地说,第一个 C++17 块中的第二个项目符号:
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.
因此根本不会创建临时副本。
后续问题:
returned 临时副本是否仍然意味着足够长的 t
生命周期——即使它保证被删除?
考虑下面给出的 foo
的变体。
我假设不再需要复制省略(但很有可能)。
如果副本不会被删除,那么标准就已经涵盖了我们(通过上面的论点)。
如果副本被删除,尽管 return
ed 表达式的类型不同于 foo
的 return 类型,标准是否仍然保证 t
的足够生命周期?
foo
-变体:
std::string foo() {
return make_tmp().c_str();
}
我想了解标准中纯粹隐含的保证。
请注意,我知道 foo
版本 "work" (即即使在各种编译器下使用自定义 类 进行测试时也不涉及悬空指针)。
我认为这里有些混淆 哪些 副本被删除了。让我们看最远的景色:
std::string make_tmp();
std::string foo() {
return std::string{make_tmp().c_str()};
}
std::string s = foo();
在这里,可能创建了四个 std::string
:make_tmp()
,从中构建的临时 std::string{...}
,return foo()
和 s
的对象。这意味着三个副本(我只是为了保持一致性而使用副本这个词,即使所有这些都是移动。希望这不会造成混淆)。
复制省略允许删除其中两个副本:
- 从
std::string{...}
复制到 foo()
的 return 对象。
- 从
foo()
复制到s
这两个省略在 C++17 的 "guaranteed copy elision" 中都是强制要求的——因为我们是从纯右值(这个术语有点令人困惑,因为我们实际上并没有执行重载决议来确定我们需要执行复制构造然后跳过它,我们直接初始化)。代码等同于:
std::string s{make_tmp().c_str()};
虽然无法删除 - 我们仍在通过 make_tmp()
构建 string
,提取其内容,然后从中构建新的 string
。没办法。
提供的变体具有完全相同的行为。
这个答案直接回答了OP中问到的lifetime issues(你可以看到它与copy elision无关)。如果你对return语句执行过程中发生的事情的始末不了解,可以参考Barry的回答。
是的,根据 [stmt.return]/2:
returned 对象的复制初始化过程中保证临时文件持续存在
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
将出现在 foo
的正文中,省略发生在 make_tmp
的正文中。所以 t
的生命周期不受省略影响 foo
的主体,无论是临时的、静态的、动态的还是其他。
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()
等同于原始代码段中的 std::string(make_tmp().c_str())
,std::string
构造函数调用隐式发生。正如您在 post 开头提到的那样,省略确实发生了。
我认为要理解省略的保证,最好理解 return 逻辑在汇编级别上的工作原理。这将使您了解编译器如何进行调用的 return 机制,这里的标准只是试图跟上实际的编译器实现,给出清晰度,而不是引入一些新的语言语法概念。
简单示例:
std::string foo();
int main() {
auto t = foo();
}
在组装相关部分时,main
主体将如下所示:
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 [=11=]x20,%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.
....
实际上发生的是 t
space 已经在调用者的 (main
's) 栈上,并且这个栈内存的地址被传递给被调用者,foo
。 foo
只需要按照里面的任何逻辑把东西放进去就可以了。 foo
可能会分配一些内存来构建 std::string
然后将此内存复制到给定的内存,但它也可能(在许多情况下这是一个简单的优化)直接在给定的内存上工作而不分配任何东西.在后者中,编译器可能会调用复制构造函数,但没有意义。 C++17 标准只是在澄清这个事实。
我试图了解 C++17 标准保证的生命周期,特别是 保证 复制省略。 让我们从一个例子开始
std::string make_tmp();
std::string foo() {
return std::string{make_tmp().c_str()};
}
我对正在发生的事情的理解:
make_tmp
创建一个临时的 string
我们将调用 t
; foo
returns 一个(不必要地创建的)临时文件(t
的 c_str
的副本)。
标准(甚至是 C++17 之前的版本)保证 t
的生命周期是整个 return 表达式被计算的时间。
因此创建 t
的临时副本是安全的(returned)。
现在 copy elisions 开始;更具体地说,第一个 C++17 块中的第二个项目符号:
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.
因此根本不会创建临时副本。
后续问题:
returned 临时副本是否仍然意味着足够长的
t
生命周期——即使它保证被删除?考虑下面给出的
foo
的变体。 我假设不再需要复制省略(但很有可能)。 如果副本不会被删除,那么标准就已经涵盖了我们(通过上面的论点)。 如果副本被删除,尽管return
ed 表达式的类型不同于foo
的 return 类型,标准是否仍然保证t
的足够生命周期?
foo
-变体:
std::string foo() {
return make_tmp().c_str();
}
我想了解标准中纯粹隐含的保证。
请注意,我知道 foo
版本 "work" (即即使在各种编译器下使用自定义 类 进行测试时也不涉及悬空指针)。
我认为这里有些混淆 哪些 副本被删除了。让我们看最远的景色:
std::string make_tmp();
std::string foo() {
return std::string{make_tmp().c_str()};
}
std::string s = foo();
在这里,可能创建了四个 std::string
:make_tmp()
,从中构建的临时 std::string{...}
,return foo()
和 s
的对象。这意味着三个副本(我只是为了保持一致性而使用副本这个词,即使所有这些都是移动。希望这不会造成混淆)。
复制省略允许删除其中两个副本:
- 从
std::string{...}
复制到foo()
的 return 对象。 - 从
foo()
复制到s
这两个省略在 C++17 的 "guaranteed copy elision" 中都是强制要求的——因为我们是从纯右值(这个术语有点令人困惑,因为我们实际上并没有执行重载决议来确定我们需要执行复制构造然后跳过它,我们直接初始化)。代码等同于:
std::string s{make_tmp().c_str()};
虽然无法删除 - 我们仍在通过 make_tmp()
构建 string
,提取其内容,然后从中构建新的 string
。没办法。
提供的变体具有完全相同的行为。
这个答案直接回答了OP中问到的lifetime issues(你可以看到它与copy elision无关)。如果你对return语句执行过程中发生的事情的始末不了解,可以参考Barry的回答。
是的,根据 [stmt.return]/2:
returned 对象的复制初始化过程中保证临时文件持续存在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
将出现在 foo
的正文中,省略发生在 make_tmp
的正文中。所以 t
的生命周期不受省略影响 foo
的主体,无论是临时的、静态的、动态的还是其他。
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()
等同于原始代码段中的 std::string(make_tmp().c_str())
,std::string
构造函数调用隐式发生。正如您在 post 开头提到的那样,省略确实发生了。
我认为要理解省略的保证,最好理解 return 逻辑在汇编级别上的工作原理。这将使您了解编译器如何进行调用的 return 机制,这里的标准只是试图跟上实际的编译器实现,给出清晰度,而不是引入一些新的语言语法概念。
简单示例:
std::string foo();
int main() {
auto t = foo();
}
在组装相关部分时,main
主体将如下所示:
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 [=11=]x20,%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.
....
实际上发生的是 t
space 已经在调用者的 (main
's) 栈上,并且这个栈内存的地址被传递给被调用者,foo
。 foo
只需要按照里面的任何逻辑把东西放进去就可以了。 foo
可能会分配一些内存来构建 std::string
然后将此内存复制到给定的内存,但它也可能(在许多情况下这是一个简单的优化)直接在给定的内存上工作而不分配任何东西.在后者中,编译器可能会调用复制构造函数,但没有意义。 C++17 标准只是在澄清这个事实。