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

Why would I ever use push_back instead of emplace_back?

提问人:David Stone 提问时间:6/5/2012 最后编辑:ildjarnDavid Stone 更新时间:8/15/2022 访问量:88009

问:

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

我必须使用什么理由?push_back

C++ C++11 标准

评论


答:

83赞 user541686 6/5/2012 #1

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

评论

23赞 Dan Albert 7/6/2013
这似乎是C++的诅咒。我们在每个新版本中都获得了大量很酷的功能,但许多公司要么为了兼容性而坚持使用一些旧版本,要么不鼓励(如果不是不允许)使用某些功能。
7赞 Dan Albert 11/14/2013
@Mehrdad:当你可以拥有伟大的东西时,为什么要满足于足够?我当然不想用 blub 编程,即使这已经足够了。并不是说这个例子就是这种情况,但作为一个为了兼容性而将大部分时间花在 C89 中编程的人,这绝对是一个真正的问题。
3赞 Mr. Boy 11/4/2015
我不认为这真的是这个问题的答案。对我来说,他要求的是更可取的用例。push_back
4赞 user541686 7/18/2016
@Mr.Boy:当你想向后兼容 C++11 之前的编译器时,这是更可取的。我的回答不清楚吗?
8赞 user541686 8/20/2017
这比我预期的要多得多,所以对于所有阅读本文的人来说:不是一个“伟大”的版本。这是一个潜在的危险版本。阅读其他答案。emplace_backpush_back
131赞 Luc Danton 6/5/2012 #2

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

struct aggregate {
    int foo;
    int bar;
};

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

另一方面,行不通。v.emplace_back({ 42, 121 });

评论

67赞 Nicol Bolas 6/5/2012
请注意,这仅适用于聚合初始化和初始值设定项列表初始化。如果您要使用语法来调用实际的构造函数,那么您可以删除 's 并使用 .{}{}emplace_back
1赞 Nicol Bolas 6/5/2012
@LucDanton:正如我所说,它仅适用于聚合初始值设定项列表初始化。您可以使用语法来调用实际构造函数。您可以给出一个接受 2 个整数的构造函数,并且在使用语法时将调用此构造函数。关键是,如果您尝试调用构造函数,则最好是,因为它会就地调用构造函数。因此,不要求类型是可复制的。{}aggregate{}emplace_back
15赞 David Stone 4/28/2016
这被视为标准中的缺陷,并已得到解决。查看 cplusplus.github.io/LWG/lwg-active.html#2089
4赞 underscore_d 1/6/2019
@DavidStone 如果它被解决,它就不会仍然在“活动”列表中......不?这似乎仍然是一个悬而未决的问题。标题为“[2018-08-23 Batavia Issues processing]”的最新更新说“P0960(目前正在飞行中)应该可以解决这个问题。而且我仍然无法编译尝试聚合的代码,而无需显式编写样板构造函数。目前还不清楚它是否会被视为缺陷并因此有资格向后移植,或者 C++ < 20 的用户是否仍将是 SoL。emplace
1赞 David Stone 8/29/2020
@underscore_d:说得好。当我发布该评论时,我误解了问题的状态。取而代之的是,这个问题已经在 C++20 中被投票通过的 open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0960r3.html 解决了。
73赞 Marc 2/10/2015 #3

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

为了适应已知的编译器错误,最好使用 if 参数引用迭代器或其他对象,这些对象在调用后将无效。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_backv[0]

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

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

注意:使用整数向量用于演示目的。我在一个更复杂的类中发现了这个问题,该类包括动态分配的成员变量,并且调用导致硬崩溃。emplace_back()

评论

15赞 Marc 3/7/2015
对 emplace_back() 的调用使用完美转发来执行就地构造,因此,直到向量调整大小后才会计算 v[0](此时 v[0] 无效)。push_back构造新元素并根据需要复制/移动元素,并在任何重新分配之前评估 v[0]。
1赞 Marc 8/7/2015
@David - 虽然新空间必须在旧空间被破坏之前存在,但我认为不能保证何时评估emplace_back参数。完美的转发使延迟评估成为可能。据我观察,在我测试的编译中评估参数之前,旧的向量迭代器会变得无效,并且细节在很大程度上取决于实现。
1赞 David Stone 8/7/2015
@Marc:标准保证emplace_back适用于范围内的元素。
5赞 Marc 12/12/2015
@cameino:存在emplace_back来延迟其参数的计算,以减少不必要的复制。该行为要么是未定义的,要么是编译器错误(等待对标准的分析)。我最近对 Visual Studio 2015 运行了相同的测试,在 Release x64 下获得了 123,3,在 Release Win32 下获得了 123,40,在 Debug x64 和 Debug Win32 下获得了 123,-572662307。
2赞 Tony Delroy 9/24/2021
@DavidStone:哦,是的,这确实有道理。谢谢。
299赞 David Stone 4/28/2016 #4

在过去的四年里,我一直在思考这个问题。我得出的结论是,大多数关于 vs. 错过了全貌。push_backemplace_back

去年,我在C++Now上做了一个关于C++14中的类型演绎的演讲。我开始谈论 vs. 在13:49,但在此之前有一些有用的信息提供了一些支持证据。push_backemplace_back

真正的主要区别在于隐式构造函数与显式构造函数。考虑以下情况:我们有一个要传递给 或 的参数。push_backemplace_back

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

在优化编译器掌握这一点后,这两个语句在生成的代码方面没有区别。传统智慧是,将构建一个临时对象,然后将其移动到其中,而将转发参数并直接将其构建到位,而无需复制或移动。根据标准库中编写的代码,这可能是正确的,但它错误地假设优化编译器的工作是生成您编写的代码。优化编译器的工作实际上是生成代码,如果你是特定于平台的优化专家,并且不关心可维护性,只关心性能,你会编写代码。push_backvemplace_back

这两个语句之间的实际区别在于,更强大的人会调用任何类型的构造函数,而更谨慎的人只会调用隐式的构造函数。隐式构造函数应该是安全的。如果你能隐式地从 a 构造一个 ,你就说它可以毫无损失地保存所有信息。在几乎任何情况下,通过一个都是安全的,如果你把它变成一个,没有人会介意。隐式构造函数的一个很好的例子是从 转换为 。隐式转换的一个坏例子是 。emplace_backpush_backUTUTTUstd::uint32_tstd::uint64_tdoublestd::uint8_t

我们希望在编程时保持谨慎。我们不想使用强大的功能,因为功能越强大,就越容易意外地做一些不正确或意想不到的事情。如果要调用显式构造函数,则需要 的 的强大功能。如果只想调用隐式构造函数,请坚持使用 。emplace_backpush_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_backvdeletenew

这不仅仅是发明的代码。这是我遇到的一个真正的生产错误。代码是 ,但它拥有内容。作为迁移到 C++11 的一部分,我正确地更改为 以指示向量拥有其内存。然而,我在 2012 年根据我的理解进行了这些更改,在此期间我想“什么都可以做,而且更多,所以我为什么要使用?”,所以我也把 改成了 .std::vector<T *>T *std::unique_ptr<T>emplace_backpush_backpush_backpush_backemplace_back

如果我让代码使用更安全的代码,我会立即发现这个长期存在的错误,并且会被视为升级到 C++11 的成功。相反,我掩盖了这个错误,直到几个月后才发现它。push_back

评论

3赞 eddi 8/10/2016
如果你能详细说明 emplace 在你的例子中到底做了什么,以及为什么它是错误的,那将会有所帮助。
9赞 David Stone 8/10/2016
@eddi:我添加了一个部分来解释这一点:有一个来自 的显式构造函数。因为可以调用显式构造函数,所以传递一个非所有权指针就可以很好地编译。但是,当超出范围时,析构函数将尝试调用该指针,该指针未分配,因为它只是一个堆栈对象。这会导致未定义的行为。std::unique_ptr<T>T *emplace_backvdeletenew
1赞 David Stone 11/1/2017
@CaptainJacksparrow:看起来我说的是隐含的和明确的。你混淆了哪个部分?
6赞 David Stone 11/2/2017
@CaptainJacksparrow:构造函数是应用了关键字的构造函数。“隐式”构造函数是没有该关键字的任何构造函数。在 的构造函数 from 的情况下,的实现者编写了该构造函数,但这里的问题是该类型的用户调用了 ,它调用了该显式构造函数。如果是 ,它将依赖于隐式转换,而不是调用该构造函数,而隐式转换只能调用隐式构造函数。explicitexplicitstd::unique_ptrT *std::unique_ptremplace_backpush_back
3赞 Konrad Rudolph 2/4/2021
很好的答案。就其价值而言,Abseil 的本周小贴士 #112 也大同小异。
0赞 Radek 8/27/2020 #5

考虑使用 c++-17 编译器在 Visual Studio 2019 中发生的情况。我们在一个设置了适当参数的函数中emplace_back。然后有人更改emplace_back调用的构造器的参数。VS 中没有警告 whatsover,代码也编译良好,然后在运行时崩溃。在此之后,我从代码库中删除了所有emplace_back。

评论

2赞 David Stone 8/29/2020
我不明白你的问题是什么。
0赞 KeyC0de 8/15/2022 #6

仅用于基元/内置类型或原始指针。否则使用 .push_backemplace_back