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

C ++ 11vector具有新的函数emplace_back 。 与依靠编译器优化来避免副本的push_back不同, emplace_back使用完美的转发将参数直接发送给构造函数以便就地创build对象。 在我看来, emplace_back所有的push_back都可以做,但有些时候它会做得更好(但从不会更糟糕)。

我有什么理由使用push_back

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

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

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

与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导致resize,在此时v[0]变为无效。

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

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

注意:使用整数的vector用于演示目的。 我发现这个问题更复杂的类,其中包括dynamic分配的成员variables和调用emplace_back()导致硬崩溃。

过去四年来我曾经想过这个问题。 我得出的结论是,大多数关于push_backemplace_back解释都没有涉及到完整的图片。

去年,我在C ++ Now上做了关于C ++ 14types扣除的演讲。 我在13:49开始讨论push_backemplace_back ,但是有一些有用的信息提供了一些支持性的证据。

真正的主要区别在于隐式和显式构造函数。 考虑一下我们想要传递给push_backemplace_back的单个参数的情况。

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

在你的优化编译器掌握了这个之后,这两个语句在生成代码方面没有区别。 传统的看法是, push_back将构造一个临时对象,然后将它移动到vemplace_back将转发该参数并直接构build它,而不需要复制或移动。 根据标准库中编写的代码,这可能是正确的,但是错误的假设是优化编译器的工作是生成您编写的代码。 优化编译器的工作实际上是生成如果您是平台特定的优化方面的专家,而不关心可维护性(即性能)的情况下编写的代码。

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

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