为返回的临时人员复制 Elision
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
返回一个(不必要的创建)临时的(t
c_str
的副本)。 标准(甚至早于 C++17)保证t
的生存期是计算完整返回表达式之前的时间。 因此,这是安全的,因此创建t
的临时副本(要返回)。
现在复制省略号开始了;更具体地说,第一个 C++17 块中的第二个项目符号:
在函数调用中,如果 return 语句的操作数是 prvalue 并且函数的返回类型与该 prvalue 的类型相同。
因此,甚至根本不会创建临时副本。
后续问题:
返回的临时副本是否仍然意味着
t
的足够延长的生存期 - 即使它保证被省略?考虑下面给出的
foo
变体。 我假设,不再需要复制省略(但很有可能)。 如果副本不会被省略,那么标准就让我们(通过上面的论点)覆盖了我们。 如果副本被省略,尽管return
ed表达式的类型与foo
的返回类型不同,标准是否仍然保证足够的t
生存期?
foo
-变体:
std::string foo() {
return make_tmp().c_str();
}
我想了解标准纯粹隐含的保证。 请注意,我知道两个foo
版本都"工作"(即,即使在各种编译器下使用自定义类进行测试时,也没有涉及悬空指针)。
我认为这里对哪些副本被省略有些困惑。让我们看最远的视图:
std::string make_tmp();
std::string foo() {
return std::string{make_tmp().c_str()};
}
std::string s = foo();
在这里,可能创建了四个std::string
:make_tmp()
、由它构造的临时std::string{...}
、foo()
的返回对象和s
。这意味着三个副本(为了保持一致性,我只是要使用单词 copy,即使所有这些都是移动。希望这不会令人困惑)。
Copy elision 允许删除其中两个副本:
- 从
std::string{...}
复制到foo()
的返回对象。 - 从
foo()
到s
的副本
这两个省略都是在 C++17 的"保证副本省略"中强制要求的 - 因为我们是从 prvalue 初始化的(这个术语有点令人困惑,因为我们实际上并没有执行重载解析来确定我们需要执行复制构造然后跳过它,我们只是直接初始化)。代码与以下内容相同:
std::string s{make_tmp().c_str()};
但这不能删除 - 我们仍然通过make_tmp()
构建一个string
,提取其内容,然后从中构建一个新的string
。没办法。
提供的变体具有完全相同的行为。
这个答案直接回答了OP中提出的生命周期问题(你可以看到它与复制省略无关)。如果你不熟悉在执行 return 语句期间发生的整个故事,你可以参考 Barry 的答案。
是的,根据 [stmt.return]/2 对返回的对象进行复制初始化期间,临时保证会持续存在:
调用结果的复制初始化在由 return 语句的操作数建立的完整表达式的末尾销毁临时表达式之前进行排序,而 return 语句又在包含 return 语句的块的局部变量 ([stmt.jump]) 销毁之前进行排序。
返回的临时副本是否仍然意味着 t 的足够延长的生存期 - 即使它被保证被省略?
t
会在foo
的身体上,而make_tmp
的身体里会发生。因此,t
的寿命不受foo
身体的影响,无论是暂时的、静态的、动态的还是其他什么。
考虑下面给出的 foo 的变体。我假设,不再需要复制省略(但很有可能)。如果副本不会被省略,那么标准就让我们(通过上面的论点)覆盖了我们。如果副本被省略,尽管返回表达式的类型与 foo 的返回类型不同,但标准是否仍然保证 t 有足够的生存期?
make_tmp().c_str()
等效于原始代码段中的std::string(make_tmp().c_str())
,则std::string
构造函数调用是隐式发生的。正如你在帖子开头提到的,省略确实发生了。
我认为要理解省略的保证,最好遵循对返回逻辑在程序集级别如何工作的理解。这会让你了解编译器是如何做出调用的返回机制的,这里的标准只是试图跟上实际的编译器实现,给出清晰度,而不是引入一些新的语言语法概念。
简单的例子:
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 $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.
....
实际上,t
空间已经在调用方(main
)的堆栈上,并且该堆栈内存的地址被传递给被调用方,foo
。foo
只需要通过其中的任何逻辑将东西放入其中,仅此而已。foo
可能会分配一些内存来构建std::string
然后将此内存复制到给定内存,但它也可能(在许多情况下是一种简单的优化)直接在给定内存上工作而不分配任何内容。在后者中,编译器可能会调用复制构造函数,但没有意义。C++17标准只是澄清了这一事实。
- C++17复制构造函数,在std::unordereded_map上进行深度复制
- 在C++程序中输入的文本文件将不起作用,除非文本被复制和粘贴
- 使用strcpy将char数组的元素复制到另一个数组
- 是否可以初始化不可复制类型的成员变量(或基类)
- 为什么在C++中使用私有复制构造函数与删除复制构造函数
- C++ Windows 驱动程序MSB3030无法复制该文件,因为它找不到
- 复制列表初始化的隐式转换的等级是多少
- 当从函数参数中的临时值调用复制构造函数时
- 有可能在Armadillo中复制MATLAB circshift方法吗
- 复制几乎为空的数组的最快方法
- 复制elision、std::move和链式函数调用
- 为返回的临时人员复制 Elision
- 复制 elision/RVO 会导致从同一对象复制/移动吗?
- C :通过STD :: simolor_ptr(仅移动类型)rvalue作为参数时复制ELISION
- C NRVO/复制Elision在括号中带有返回语句
- 在C 14允许它的情况下,C 17禁止复制ELISION
- 自 C++17 以来,复制 elision 不需要存在和访问复制或移动 CTOR
- 函数返回一个不使用复制/移动或复制ELISION的值
- 复制 ELISION:在 return 语句中使用三元表达式时未调用移动构造函数
- 如何强制执行复制 elision,为什么它不适用于已删除的复制构造函数?