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

c++ 11向量有了新函数emplace_back。与依赖编译器优化来避免复制的push_back不同,emplace_back使用完全转发将参数直接发送给构造函数以就地创建对象。在我看来,emplace_back做了push_back能做的所有事情,但有时它会做得更好(但绝不会更差)。

我为什么要使用push_back?

78649 次浏览

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

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

struct aggregate {
int foo;
int bar;
};


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

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

emplace_back的一些库实现行为不符合c++标准(包括Visual Studio 2012、2013和2015发布的版本)中指定的行为。

为了适应已知的编译器错误,如果参数引用迭代器或其他在调用后无效的对象,最好使用__abc0。

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]);

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

在过去的四年里,我经常思考这个问题。我得出的结论是,大多数关于push_backemplace_back的解释都没有全貌。

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

真正主要的区别在于隐式构造函数和显式构造函数。考虑这样一种情况,我们有一个参数,想要传递给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的转换。一个不好的隐式转换的例子是push_back0到push_back1。

我们希望在编程时保持谨慎。我们不想使用功能强大的功能,因为功能越强大,就越容易意外地做一些不正确或意想不到的事情。如果你打算调用显式构造函数,那么你需要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的成功。相反,我掩盖了这个漏洞,直到几个月后才发现它。

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

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