为什么我会使用push_back而不是emplace_back?

Dav*_*one 215 c++ std c++11

C++ 11向量具有新功能emplace_back.与push_back依赖于编译器优化以避免副本的情况不同,emplace_back使用完美转发将参数直接发送到构造函数以就地创建对象.在我看来,emplace_back一切都push_back可以做到,但有些时候它会做得更好(但从来没有更糟).

我有什么理由使用push_back

Dav*_*one 143

在过去的四年里,我对这个问题进行了相当多的考虑.我得出的结论是,关于push_backvs.的大部分解释都emplace_back错过了全貌.

去年,我在C++中演示了C++中的类型演绎14.我在13:49 开始谈论push_backvs. emplace_back,但有一些有用的信息在此之前提供了一些支持证据.

真正的主要区别与隐式和显式构造函数有关.考虑我们想要传递给push_back或的单个参数的情况emplace_back.

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);
Run Code Online (Sandbox Code Playgroud)

在优化编译器得到它之后,就生成的代码而言,这两个语句之间没有区别.传统的观点认为,push_back将构造一个临时对象,然后将得到搬进vemplace_back将转发沿参数,没有复制或移动到位直接构造它.基于标准库中编写的代码,这可能是正确的,但它会错误地假设优化编译器的工作是生成您编写的代码.优化编译器的工作实际上是生成您编写的代码,如果您是特定于平台的优化专家,并且不关心可维护性,只关心性能.

这两个语句之间的实际区别在于,更强大的emplace_back将调用任何类型的构造函数,而更谨慎的push_back只调用隐式的构造函数.隐含的构造函数应该是安全的.如果你可以隐含地构造一个U来自a T,你就是说U可以保存所有信息T而不会丢失.在几乎任何情况下都可以安全地通过,T而且没有人会介意的话U.隐式构造函数的一个很好的例子是转换std::uint32_tstd::uint64_t.的隐式转换的一个坏的例子是doublestd::uint8_t.

我们希望在编程时保持谨慎.我们不想使用强大的功能,因为功能越强大,就越容易意外地做出错误或意外的事情.如果你打算调用显式构造函数,那么你需要它的强大功能emplace_back.如果你只想调用隐式构造函数,请坚持使用push_back.

一个例子

std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile
Run Code Online (Sandbox Code Playgroud)

std::unique_ptr<T>有一个明确的构造函数T *.因为emplace_back可以调用显式构造函数,所以传递一个非拥有指针就可以了.但是,当v超出范围时,析构函数将尝试调用delete该指针,该指针未被分配,new因为它只是一个堆栈对象.这导致未定义的行为.

这不仅仅是发明的代码.这是我遇到的真正的生产错误.代码是std::vector<T *>,但它拥有内容.作为迁移到C++ 11的一部分,我正确地改变T *,以std::unique_ptr<T>指示该矢量拥有它的存储器.不过,我在2012年基础这些改变了我的理解,在这期间我还以为"emplace_back做一切的push_back可以做多,所以我为什么会用不完的push_back?",所以我也改变了push_backemplace_back.

如果我把代码保留为使用更安全push_back,我会立即抓住这个长期存在的错误,它将被视为升级到C++ 11的成功.相反,我掩盖了这个错误,直到几个月后才找到它.

  • @eddi:我添加了一节解释这个:`std :: unique_ptr <T>`有一个来自`T*`的显式构造函数.因为`emplace_back`可以调用显式构造函数,所以传递一个非拥有指针就可以了.但是,当`v`超出范围时,析构函数将尝试在该指针上调用`delete`,该指针不是由`new`分配的,因为它只是一个堆栈对象.这导致未定义的行为. (4认同)
  • 如果您能详细说明 emplace 在您的示例中到底做了什么以及为什么它是错误的,那将会有所帮助。 (3认同)
  • 很好的答案。无论如何,Abseil 的[本周提示 #112](https://abseil.io/tips/112) 也提出了大致相同的观点。 (3认同)
  • @CaptainJacksparrow:“ explicit”构造函数是将关键字“ explicit”应用于其的构造函数。“隐式”构造函数是没有该关键字的任何构造函数。对于来自T *的std :: unique_ptr的构造函数,std :: unique_ptr的实现者编写了该构造函数,但是这里的问题是该类型的用户称为emplace_back,该显式构造函数。如果它是`push_back`,而不是调用该构造函数,它将依靠隐式转换,该转换只能调用隐式构造函数。 (2认同)

Luc*_*ton 114

push_back总是允许使用统一初始化,这是我非常喜欢的.例如:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });
Run Code Online (Sandbox Code Playgroud)

另一方面,v.emplace_back({ 42, 121 });不会起作用.

  • 请注意,这仅适用于聚合初始化和初始化列表初始化.如果你使用`{}`语法来调用实际的构造函数,那么你可以删除`{}`并使用`emplace_back`. (55认同)
  • 这被视为标准中的缺陷,并已得到解决.请参阅https://cplusplus.github.io/LWG/lwg-active.html#2089 (10认同)
  • @DavidStone如果它已被解决,它将不会仍然处于"活动"列表中......不是吗?它似乎仍然是一个悬而未决的问题.最新更新,标题为"_ [2018-08-23巴达维亚问题处理] _",表示"_ [P0960](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/ 2018/p0960r1.html)(目前正在进行中)应该解决这个问题.如果没有明确编写样板构造函数,我仍然无法编译试图`emplace`聚合的代码.目前还不清楚它是否会被视为缺陷并因此有资格进行反向移植,或者C++ <20的用户是否仍然是SoL. (3认同)
  • @underscore_d:非常好的一点。当我发表该评论时,我误解了问题的状态。相反,这个问题已在 C++20 中由 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0960r3.html 解决,并被投票纳入标准。 (2认同)

Meh*_*dad 79

向后兼容前C++ 11编译器.

  • 这似乎是C++的诅咒.我们在每个新版本中都获得了很多很酷的功能,但是为了兼容性或者阻止(如果不禁止)使用某些功能,许多公司要么使用旧版本. (20认同)
  • @Mehrdad:为什么在你能拥有伟大的时候能够满足?我肯定不想在[blub](http://www.paulgraham.com/avg.html)中编程,即使它已经足够了.不是说特别是这个例子的情况,但是为了兼容性而花费大部分时间在C89编程的人,这绝对是一个真正的问题. (6认同)
  • 这比我预期的要多得多,所以你们所有人都读到这个:"emplace_back"不是**"push_back"的"伟大"版本.它是一个潜在的危险**版本.*阅读其他答案.* (5认同)
  • 我认为这不是问题的答案.对我来说,他要求使用`push_back`更好的用例. (3认同)
  • @ Mr.Boy:当你想要与C++ 11之前的编译器向后兼容时,这是更好的选择.在我的回答中不清楚吗? (2认同)

Mar*_*arc 67

emplace_back的某些库实现的行为不符合C++标准中的规定,包括Visual Studio 2012,2013和2015附带的版本.

为了适应已知的编译器错误,std::vector::push_back()如果参数引用迭代器或在调用后无效的其他对象,则更喜欢使用.

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers
Run Code Online (Sandbox Code Playgroud)

在一个编译器上,v包含值123和21而不是预期的123和123.这是因为第二次调用emplace_back导致调整大小,此时该值v[0]变为无效.

上述代码的工作实现将使用push_back()而不是emplace_back()如下:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);
Run Code Online (Sandbox Code Playgroud)

注意:使用int的向量是为了演示目的.我发现这个问题有一个更复杂的类,其中包括动态分配的成员变量和调用emplace_back()导致硬崩溃.

  • 对emplace_back()的调用使用完美转发来执行构造,因此直到向量调整大小之后才评估v [0](此时v [0]无效).push_back构造新元素并根据需要复制/移动元素,并在任何重新分配之前评估v [0]. (11认同)
  • @cameino:emplace_back存在以延迟评估其参数以减少不必要的复制.行为未定义或编译器错误(待标准分析).我最近对Visual Studio 2015运行了相同的测试,在Release x64下获得了123,3在Release Win32下获得了123,40,在Debug x64和Debug Win32下获得了123,-572662307. (4认同)
  • @DavidStone:哦,是的,这确实有道理。谢谢。 (2认同)