为什么“保证复制消除”不意味着 push_back({arg1, arg2}) 与 emplace_back(arg1, arg2) 相同?

Ell*_*ith 4 c++ language-lawyer copy-elision c++17 prvalue

首先,我听说“保证复制消除”是一个用词不当(据我目前的理解,它更多的是从根本上重新定义基本值类别,从 r/l 值到 l/x/pr 值,这从根本上改变了含义和副本的要求),但由于这就是通常所说的,我也会。

在阅读了有关这个主题的一些内容后,我想我终于理解了它 - 至少足以认为:

my_vector.push_back({arg1, arg2});
Run Code Online (Sandbox Code Playgroud)

从 c++17 开始,相当于:

my_vector.emplace_back(arg1, arg2);
Run Code Online (Sandbox Code Playgroud)

我最近试图说服我的同事这一点。唯一的问题是他告诉我我完全错了!他编写了一些 godbolt 代码(如下所示),其中程序集显示push_back创建了一个临时文件,该临时文件被移入向量中。


因此,为了完成我的问题,我必须首先证明这里存在一些令人困惑的原因。我将引用关于这个主题的备受推崇的 stackoverflow 答案(强调我的):

保证复制省略重新定义了纯右值表达式的含义。[...]纯右值表达式只是可以实现临时值的东西, 但它还不是临时值

如果使用纯右值来初始化该纯右值类型的对象, 则不会具体化临时值。[...]

需要理解的是,由于返回值是纯右值, 因此它还不是对象。它只是一个对象的初始化器[...]

就我而言,我认为auto il = {arg1, arg2}会调用 for 的构造函数std::initializer_list,但{arg1, arg2}inpush_back({arg1, arg2})将是纯右值(因为它未命名),因此将是向量元素的初始化器,而无需对其进行初始化。

当你执行 T t = Func(); 时,返回值的纯右值直接初始化对象 t;没有“创建临时和复制/移动”阶段。由于 Func() 的返回值是与 T() 等效的纯右值,因此 t 直接由 T() 初始化,就像执行 T t = T() 一样。

如果以任何其他方式使用纯右值,则纯右值将具体化一个临时对象,该对象将在该表达式中使用(如果没有表达式,则将其丢弃)。因此,如果您执行 const T &rt = Func();,纯右值将实现一个临时值(使用 T() 作为初始值设定项),其引用将与通常的临时生命周期扩展内容一起存储在 rt 中。

保证省略也适用于直接初始化

有人可以向我解释一下为什么保证复制消除不适用于我的示例吗?

use*_*522 11

但是 push_back({arg1, arg2}) 中的 {arg1, arg2} 将是纯右值(因为它未命名),因此将是向量对象的初始化器,而无需对其自身进行初始化。

我假设“向量对象”在这里指的是向量元素,该对象将存储在由 管理的存储中vector,并且push_back/emplace_back应该将其添加到它。

{arg1, arg2}它本身不是一个表达式,它只是一个braced-init-list,一个不同的语法结构。所以它本身没有价值类别。然而,它有关于如何在重载解析中起作用以及如何初始化对象和引用的规则。

选择的过载push_back将是

void push_back(value_type&&);
Run Code Online (Sandbox Code Playgroud)

其中value_type是向量的元素类型。此重载中的引用参数需要引用某个类型的对象value_type。因此,必须使用花括号初始化列表来构造一个类型的(临时)对象value_type来绑定此引用。但是,这不可能是存储在向量中的对象,因为它是在调用者上下文中创建的临时对象。调用者不知道在哪里 push_back构造向量的实际元素。因此,push_back需要从绑定到参数引用的临时对象到放置在向量存储中的实际对象进行移动构造。

因此实际上my_vector.push_back({arg1, arg2});与 相同my_vector.push_back(value_type{arg1, arg2});,只是它是一个实际的纯右值表达式,当从中value_type{arg1, arg2}初始化 的引用参数时,它将具体化为临时对象。push_back由于这种几乎相同的行为,人们可能会草率地说{arg1, arg2}“是纯右值”或“是临时的”,即使这在技术上是不正确的。

push_back没有任何不引用 a 的重载value_type,所以这总是不可避免的。emplace_back另一方面,将任何类型作为参数,然后将它们直接转发到存储在向量存储中的对象的构造。

转发花括号初始化列表也是不可能的。没有语法可以在保留各个列表元素的类型的同时捕获它们。您只能从整个列表中初始化指定类型的对象,就像push_back初始化数组或std::initializer_list同构元素类型(每个列表元素中的一个元素)一样,这就是初始化列表构造函数要做的事情(同构类型是向量的元素类型)。