vector中emplace_back与push_back的不同

前言

std::vector中有两个函数都能向容器尾部添加元素,分别是push_backemplace_back

在发展历史上,emplace_backpush_back后推出,以解决push_back中存在的一些效率问题。

我们现在引出三个问题:

  • emplace_back能否全面代替push_back
  • emplace_back是否在所有场景下均快于push_back
  • emplace_back有哪些隐藏的坑

push_backemplace_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_backemplace_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_backpush_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_backemplace_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_backemplace_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

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
隐藏
变装