前言
std::vector
中有两个函数都能向容器尾部添加元素,分别是push_back
和emplace_back
在发展历史上,emplace_back
较push_back
后推出,以解决push_back
中存在的一些效率问题。
我们现在引出三个问题:
emplace_back
能否全面代替push_back
emplace_back
是否在所有场景下均快于push_back
emplace_back
有哪些隐藏的坑
push_back
与emplace_back
的函数声明
push_back
constexpr // C++20
void push_back(const T& value);
constexpr // C++20
void push_back(T&& value); // C++11
emplace_back
在C++17前
template<typename ...Args>
void emplace_back(Args&&... args);
在C++17及以后
template<typename ...Args>
constexpr // C++20
reference emplace_back(Args&&... args);
从上面的声明中可以看出,push_back
是接受容器内元素类型的对象作为实参,而emplace_back
则是接受构造容器内元素的参数作为实参。
换而言之,push_back
最终将构造的对象放入容器中时会调用对象的移动构造或者复制构造函数,而emplace_back
除了会调用对象的移动构造或者复制构造函数,还可以直接使用参数构造对象。
这里我们就发现了一个问题:
push_back
的参数类型对于每一个std::vector<T>
类型的对象是确定的,而emplace_back
的参数类型则依赖于函数模板推导。
一般场景下的性能差异
我们先定义一个结构体:
struct A
{
A()
{ std::cout << "A" << "\n"; }
A(int)
{ std::cout << "A" << "\n"; }
A(const A&)
{ std::cout << "A&" << "\n"; }
A(A&&)
{ std::cout << "A&&" << "\n"; }
};
然后分别使用push_back
和emplace_back
来操作:
push_back
std::vector<A> vec;
A a;
vec.push_back(a);
vec.push_back(1);
输出:
A&
A
A&&
emplace_back
emplace_back(a);
emplace_back(A(1));
emplace_back(1);
输出:
A&
A
A&&
A
显然,当我们打算创建一个临时对象并且打算将这个临时对象插入容器尾部时,emplace_back(<parameter-list>)
是一个更好的选择,在传入的对象是T&
和T&&
类型的情况下emplace_back
与push_back
通常情况下没有差别。
emplace_back
能否覆盖push_back
的所有使用场景?
由于emplace_back
参数依赖模板推导,所以当我们想使用std::initializer_list
来初始化vector
中的元素时就会出现问题:
std::vector<std::vector<int>> vec;
vec.emplace_back({1, 2, 3, 4}); // error
这里的大括号会被认为是一般的列表初始化,但编译器无法从这个列表初始化中推断出需要构造的类型。
解决方法是换用push_back
(前提是元素类型的构造函数非explicit)或者显示地构造一个std::initializer_list
。
出于同样的理由,当我们调用:
std::vector<std::string> vec;
vec.push_back("111");
vec.emplace_back("111");
时,我们实际上调用的是:
void push_back(const std::string&);
void emplace_back(const char (&) [4]);
这也就意味着,当我们传入一个字符串常量时,push_back
总会调用push_back(const std::string&)
,而emplace_back
则会调用不同的emplace_back(const char (&) [N])
(N取决于推导的字符串长度)
这会增加生成的代码体积,也会拖累代码的执行效率。
同时,在std::vector
存储智能指针对象时,push_back
与emplace_back
也存在差异
我们要向容器尾部添加一个unique_ptr<T>
类型的对象,现在有两种方法:
vec.push_back(std::unique_ptr<T>(new T));
vec.emplace_back(new T);
这两个函数的差异在于被创建出来的指针的所有者的不同。
使用push_back
时,new T
创建出来的指针立即给到了临时对象,也就意味着对new T
的生存期与其所属的std::unique_ptr<T>
对象生存期绑定的保证。
使用emplace_back
时,new T
创建出来的指针先是作为函数参数,最后传递到allocator_traits<vector<std::unique_ptr<T>>::allocator_type>::construct()
函数进行对象的构造,也就是说new T
并没有第一时间给予std::unique_ptr<T>
对象管理,一旦construct
函数的调用过程或者emplace_back
调用construct
函数前的过程出现问题,就可能导致new T
的内存泄露。
换而言之,这降低了异常安全性。
一些使用emplace_back
的坑
当一个类型的构造器参数列表是:
explicit A(const char* str);
那么
push_back(nullptr);
无法通过编译,除非去除explicit
然而
emplace_back(nullptr);
可以通过编译,虽然emplace_back
被设计出来有这种预期,但在这一场景下我们显然不希望传入的字符串是空指针,这意味着当我们向emplace_back
传入错误的实参时可能导致一些无法预测的行为。
总结
emplace_back
可以用于避免临时对象的创建(或者说直接在容器内构造对象),同时也应用于需要隐式地创建对象时(尤其是元素类型的构造函数为explicit)
例如:push_back({1}); // 有临时对象的创建 push_back(A{1}); // 元素没有非explicit的构造函数
就可以替换为
emplace_back(1); // 既隐式地构造了对象,还避免了临时对象的创建
- 当传入的参数类型与元素类型不一样时,使用
push_back
会调用类型转换函数,进而得到元素类型的对象,而emplace_back
则会尝试调用构造函数,从而避免转换。 - 在一般情况下,当我们已经拥有元素类型的对象时,
push_back
与emplace_back
无差异 - 在需要考虑生成代码体积的情况下,需要谨慎地使用
emplace_back
- 同时,由于
emplace_back
构造对象的过程封装在实现函数中,它的错误提示可能比push_back
更加不友好,并且由于emplace_back
可以接受用于构造元素的实参,当传入错误实参后并编译通过后的行为难以预测,需要更加小心的使用(详见Effective Modern C++) - 使用智能指针时,更严谨地说是用于管理资源的类型对象时(同样参考Effective Modern C++),更多地使用
push_back
而非emplace_back
参考
[1] 知乎:C++ 的 emplace_back 能完全代替 push_back 吗?
[2] 知乎:C++中push_back和emplace_back的区别
[3] cntransgroup.github.io: Effective Modern C++ Chinese Item 42