欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

c++11中emplace_back vs push_back

程序员文章站 2022-03-22 21:02:17
...

引言


在C11中,有两种方法可以把元素放入容器中:emplace_back和push_back。

push_back是C11之前就有的,而emplace_back是C11中新加的。

既然它们的作用都是一样的,那么为什么C11中又加入了一个emplace_back?

既生瑜,何生亮?

在实际的项目编码中,到底用哪个呢?

优先选用emplace_back


考虑下面这段非常常见的代码:

std::vector<string> vs;

vs.push_back("xyz");

其中,vs容器持有的是string类型的对象,而在push_back的时候传入的是字符串字面量(不是string),换句话说,传递给push_back的实参并非容器持有物的类型。

同时我们也知道,这段代码没有问题,能够通过编译并正常运行。但是它背后都执行了哪些操作呢?

我们来看一下vector的push_back,它针对左值和右值给出了不同的重载版本:

template<class T, class Allocator = alloctor<T>> // c++11标准
class vector
{
	public:
		// ...
		void push_back(const T& x); // 左值
		void push_back(T&& x); // 右值
		// ...
};

现在回头看一下上面的那段代码,它会从字符串字面量出发创建string类型的临时对象,并将该临时对象传递给push_back,如下:

vs.push_back(string("xyz"));

之所以要说明一下push_back后面的一些过程,是为了说明上面这句代码,虽然在功能上没有任何问题,但它可能存在性能上问题,因为它共执行了2次构造和1次析构:

  • 从字符串字面量“xyz”,创建string临时对象。临时对象没有名字,可以称为temp。这是第一次构建,因为是临时对象,所以temp是右值。
  • temp传递给push_back的右值重载版本,它被绑定到右值引用形参x。然后会在内存中为vector构造一个x的副本,这是第2次构造,在vector内创建了一个新的对象。
  • 在push_back返回的时候,temp析构。

那么,有没有方法能将字符串字面量直接传递给vector内构造的string对象,从而避免temp对象的构造和析构呢?

有!emplace_back就可以!

emplace_back使用完美转发,它使用传入的任何实参,在vector内构造一个string,不会涉及任何临时对象。

emplace_back之所以比push_back更牛逼,是因为它提供了更加灵活的接口:

  • push_back接受的是待插入对象
  • emplace_back接受的是待插入对象的构造函数实参

所以emplace_back能够避免临时对象的创建和析构。

即使在push_back燕不要求创建临时对象的情况下,也可以使用emplace_back,这时,它们两个做的是同一件事情。

综上所述,emplace_back能做到push_back所能够做到的一切事情,而且前者可能比后者更高效,那么,何不总是使用emplace_back呢?

push_back的用武之地


然而,emplace_back比push_back的效率有时更高,是理论上的。

在实际实践中,还是要根据看传递的实参类型、容器种类、插入位置、容器持有类型构造函数的异常安全性、容器是否禁止相同元素的插入等情况,对两者性能进行基准测试。

根据经验,如果下列情况都成立,那么emplace_back几乎总是比push_back的效率更高:

  • 待添加的值是以构造而非赋值方式加入容器,这个大多数标准容器都满足
  • 传递的实参类型与容器持有之物的类型不同,这时emplace_back不要求创建和析构临时对象
  • 容器不太可能由于出现重复情况而拒绝待添加的值,要么容器允许重复值,要么添加的大部分值满足唯一性。因为emplace_back会创建临时节点与容器内值比较,如果因为重复值被拒绝,构造与析构就付出了成本。

除此之外,如果要使用emplace_back,还有两个问题需要处理:

  • 涉及到资源管理。push_back会存在临时对象,当构造抛出异常时,临时对象可以正常析构释放对象
  • 与带有explicit声明饰词的构造函数之间的互动。在使用emplace_back,要特别小心去保证传递了正确的实参,因为即使是带有explicit声明饰词的构造函数也会被编译器纳入考虑范围,因为它会尽力找到某种方法来解释你的代码使它合法(即使不符合逻辑)。

以上,在emplace_back不适合使用的时候,就是push_back的用武之地了。

由于它内在机制的不同,自然也就不会造成那些隐晦的、难以调试的bug。

小结


C11可以使用emplace_back和push_back向容器内添加元素。

建议优先选用emplace_back,因为它几乎能做到push_back所能做到的一切,且可能避免性能上的一些潜在问题。

但emplace_back在一些场景下也有使用陷阱,如异常安全性、能够通过编译但可能导致未定义的行为等,在这些场合下使用push_back就可以避免这些问题。

在具体的使用场景下,选用何种方式来实现,以及真正的性能差异,需要进行基准测试。

参考资料

《Effective Modern C++》