为什么我会用push_back而不是emplace_back

Why would I ever use push_back instead of emplace_back?

本文关键字:back emplace push 为什么      更新时间:2023-10-16

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

我有什么理由使用push_back

在过去的

四年里,我对这个问题进行了相当多的思考。我得出的结论是,大多数关于push_backemplace_back错过了全貌。

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

真正的主要区别与隐式构造函数与显式构造函数有关。考虑一下我们有一个参数的情况,我们想传递给push_backemplace_back

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

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

这两个语句之间的实际区别在于,更强大的emplace_back将调用任何类型的构造函数,而更谨慎的push_back将仅调用隐式构造函数。隐式构造函数应该是安全的。如果你能从T隐式构造一个U,你就是在说U可以把所有的信息都保存在T而不会丢失。几乎在任何情况下通过T都是安全的,没有人会介意你把它变成U。隐式构造函数的一个很好的例子是从 std::uint32_tstd::uint64_t 的转换。隐式转换的一个不好的例子是double std::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_back改为emplace_back

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

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

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

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

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

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。这是因为第二次调用emplace_back会导致调整大小,此时v[0]将变为无效。

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

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

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

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

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