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


231

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

我必须使用什么原因push_back

Answers:


161

在过去的四年中,我已经对这个问题进行了很多思考。我得出的结论是,大多数关于push_backvs.的解释都emplace_back没有完整描述。

去年,我在C ++ Now上发表了有关C ++ 14中类型归纳的演讲。我从13:49 开始讨论push_backvs. emplace_back,但是在此之前有有用的信息提供了一些支持证据。

真正的主要区别与隐式和显式构造函数有关。请考虑以下情况:只有一个参数要传递给push_backemplace_back

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);

在您的优化编译器掌握了这一点之后,就生成代码而言,这两个语句之间没有任何区别。传统观点认为,push_back将构造一个临时对象,然后将其移入其中,vemplace_back将继续传递论点并直接将其构造成没有副本或移动的位置。基于标准库中编写的代码,这可能是正确的,但是它错误地假设了优化编译器的工作是生成您编写的代码。如果您是平台特定优化方面的专家并且不关心可维护性,而只是关注性能,那么优化编译器的工作实际上是生成您将编写的代码。

这两个语句之间的实际区别是,功能强大的emplace_back将调用那里的任何类型的构造函数,而谨慎的push_back调用将仅调用隐式的构造函数。隐式构造函数应该是安全的。如果您可以U从隐式构造a T,则表示U可以T毫无损失地保存所有信息。在几乎任何情况下,通过a都是安全的,T如果您U改为a ,则没人会介意。隐式构造函数的一个很好的例子是从std::uint32_t到的转换std::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

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的成功。相反,我掩盖了该错误,直到几个月后才发现它。


如果您可以详细说明示例中Emplace的确切功能以及错误原因,这将对您有所帮助。
埃迪(EDDI)2016年

4
@eddi:我添加了解释这一点的部分:std::unique_ptr<T>具有来自的显式构造函数T *。因为emplace_back可以调用显式构造函数,所以传递非所有者指针可以很好地进行编译。但是,当v超出范围时,析构函数将尝试调用delete该指针,该指针未被分配,new因为它只是一个堆栈对象。这导致未定义的行为。
大卫·斯通

感谢您发布此信息。我写答案时并不知道,但是现在我希望自己以后再学时就自己写:)我真的很想拍打那些改用新功能的人,只是为了做他们能找到的最时髦的事情。伙计们,人们也在C ++ 11之前使用C ++,但并非所有问题都是有问题的。如果您不知道为什么要使用功能,请不要使用它。很高兴您发布了这篇文章,我希望它能获得更多好评,从而超越我的范围。+1
user541686 '17

1
@CaptainJacksparrow:好像我说的是隐式和显式。您对哪一部分感到困惑?
大卫·斯通

4
@CaptainJacksparrow:explicit构造函数是explicit应用了关键字的构造函数。“隐式”构造函数是没有该关键字的任何构造函数。在std::unique_ptrfrom的构造函数的情况下T *,的实现者std::unique_ptr编写了该构造函数,但是这里的问题是这种类型的用户称为emplace_back,该用户称为该显式构造函数。如果为push_back,则无需调用该构造函数,而是依靠隐式转换,该转换只能调用隐式构造函数。
大卫·斯通

116

push_back总是允许使用我很喜欢的统一初始化。例如:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

另一方面,v.emplace_back({ 42, 121 });将无法正常工作。


58
请注意,这仅适用于聚合初始化和初始化列表初始化。如果要使用{}语法来调用实际的构造函数,则只需删除{}并使用即可emplace_back
Nicol Bolas 2012年

愚蠢的提问时间:所以emplace_back根本不能用于结构向量吗?还是不适合使用文字{42,121}的样式?
Phil H

1
@LucDanton:正如我所说,它仅适用于聚合初始化列表初始化。您可以使用{}语法来调用实际的构造函数。您可以提供aggregate一个使用2个整数的构造函数,并且在使用{}语法时会调用此构造函数。关键是,如果您尝试调用构造函数,那emplace_back将是更好的选择,因为它可以就地调用构造函数。因此,不需要类型是可复制的。
Nicol Bolas 2012年

11
这被视为标准中的缺陷,并且已得到解决。参见cplusplus.github.io/LWG/lwg-active.html#2089
David Stone

3
@DavidStone如果已解决,它将不会仍在“活动”列表中……否?它似乎仍然是一个悬而未决的问题。标题为[[2018-08-23 Batavia问题正在处理中]的最新更新表示,“ P0960(当前正在运行中)应解决此问题。 ”而且,emplace如果不显式编写样例构造函数,我仍然无法编译试图聚合的代码。目前还不清楚是否将其视为缺陷并因此有资格进行反向移植,或者C ++ <20的用户是否仍将保留SoL。
underscore_d

80

与C ++ 11之前的编译器向后兼容。


21
那似乎是C ++的诅咒。我们在每个新版本中都提供了许多很棒的功能,但是出于兼容性的考虑,很多公司要么坚持使用某些旧版本,要么不鼓励(如果不允许的话)禁止使用某些功能。
丹·艾伯特

6
@Mehrdad:为什么当你拥有伟大的东西时就满足于足够?即使足够,我肯定也不想在blub中进行编程。并不是说这个例子就是这种情况,而是因为出于兼容性的原因,大部分时间都花在C89上编程,所以这绝对是一个现实问题。
丹·艾伯特

3
我不认为这确实是问题的答案。对我来说,他在寻找push_back更好的用例。
男孩先生2015年

3
@ Mr.Boy:当您想与C ++ 11以前的编译器向后兼容时,它是首选。我的回答不清楚吗?
user541686 '16

6
这已经得到的方式更多的关注比我预期的,所以对于大家阅读本:emplace_back不是一个“伟大”的版本push_back。这是一个潜在的危险版本。阅读其他答案。
user541686

68

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

在一个编译器上,v包含值123和21而不是预期的123和123。这是由于以下事实:对2的调用emplace_back导致调整大小,此时调整大小v[0]变为无效。

上述代码的有效实现将使用push_back()而不是emplace_back()如下所示:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

注意:使用整数向量仅用于演示目的。我发现了一个更为复杂的类,这个问题包括动态分配的成员变量以及对的调用emplace_back()导致了严重的崩溃。


等待。这似乎是一个错误。如何能push_back在这种情况下,有什么不同?
巴尔基2015年

12
对emplace_back()的调用使用了完美的转发来进行适当的构造,因此,在调整向量的大小之后(此时v [0]无效),不会评估v [0]。push_back构造新元素,并根据需要复制/移动该元素,并在重新分配之前评估v [0]。
2015年

1
@Marc:通过标准保证,即使对于范围内的元素,emplace_back也可以使用。
大卫·斯通

1
@DavidStone:请提供参考,以确保在标准中此行为在哪里?无论哪种方式,Visual Studio 2012和2015都会表现出错误的行为。
Marc 2015年

4
@cameino:存在emplace_back可以延迟对其参数的评估,以减少不必要的复制。该行为是未定义的,也可能是编译器错误(正在分析标准)。我最近对Visual Studio 2015运行了相同的测试,在x64版本下获得了123,3,在Win32版本下获得了123,40,在Debug x64和Debug Win32下获得了123,-572662307。
Marc
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.