===================
== lxulxu's blog ==
===================
Hello there

C++中的push_back与emplace_back

c++

1. 引言

C++标准库提供了push_backemplace_back两种向容器末尾添加元素的方法。本文将深入分析这两个函数的区别、使用场景,以及在实际应用中的性能考虑。

2. 基本概念

2.1 push_back push_back有两个重载版本:

1void push_back(const T& value);
2void push_back(T&& value);

第一个版本复制元素,第二个版本移动元素。

2.2 emplace_back emplace_back是C++11引入的变参模板函数:

1template <class... Args>
2void emplace_back(Args&&... args);

它直接在容器中构造对象,参数被完美转发给元素的构造函数。

3. 主要区别

  1. 构造方式push_back需要预先构造的对象,emplace_back在容器内构造对象。
  2. 参数传递push_back接受对象,emplace_back接受构造函数参数。
  3. 效率emplace_back可能避免不必要的临时对象创建和复制/移动操作。
  4. 灵活性emplace_back可直接传递构造函数参数。
  5. 编译复杂度emplace_back作为变参模板可能增加编译时间和内存使用。

4. 使用示例

4.1 简单类型

1std::vector<int> vec;
2vec.push_back(10);  // push_back 足够简单高效

4.2 复杂对象构造

1std::vector<std::pair<int, std::string>> vec;
2// 使用 push_back
3vec.push_back(std::make_pair(1, "one"));
4// 使用 emplace_back
5vec.emplace_back(1, "one");  // 更简洁,直接传递构造函数参数

4.3 不可移动类型

1std::vector<std::mutex> mutexes;
2mutexes.emplace_back();  // 可以工作,直接在容器中构造 mutex
3// mutexes.push_back(std::mutex()); // 编译错误,mutex 不可复制或移动

5. 性能考虑:emplace_back并非总是更优

虽然通常认为 emplace_back 在性能上优于 push_back,但实际情况可能并非如此简单。

5.1 理论上的优势

 1class MyClass {
 2public:
 3    MyClass(int a, double b) : x(a), y(b) {
 4        std::cout << "MyClass constructed\n";
 5    }
 6    MyClass(const MyClass&) {
 7        std::cout << "MyClass copied\n";
 8    }
 9    MyClass(MyClass&&) noexcept {
10        std::cout << "MyClass moved\n";
11    }
12private:
13    int x;
14    double y;
15};
16
17std::vector<MyClass> vec;
18vec.push_back(MyClass(10, 3.14));  // 构造 + 移动
19vec.emplace_back(10, 3.14);  // 直接构造,理论上更高效

理论上,emplace_back 通过直接在容器内构造对象,避免了额外的移动操作,因此应该更高效。

5.2 编译器优化的影响

1std::vector<std::string> vec;
2vec.push_back(std::string("Hello"));  // 可能被优化,避免额外复制
3vec.emplace_back("World");  // 直接构造

现代编译器的优化能力可能会显著减小 push_backemplace_back 之间的性能差距。

5.3 C++17及以后版本的改进

1struct Expensive {
2    Expensive() { std::cout << "Constructed\n"; }
3    Expensive(const Expensive&) { std::cout << "Copied\n"; }
4    Expensive(Expensive&&) noexcept { std::cout << "Moved\n"; }
5};
6
7std::vector<Expensive> vec;
8vec.push_back(Expensive());  // C++17: 直接构造,不会调用移动构造函数
9vec.emplace_back();          // 直接构造

C++17引入的保证复制省略(guaranteed copy elision)进一步模糊了 push_backemplace_back 之间的界限。

5.4 性能结论

尽管 emplace_back 在某些情况下确实可能提供性能优势,但这种优势并不像人们通常认为的那样普遍或显著。实际上,由于编译器优化和语言标准的演进,在许多常见情况下,push_backemplace_back 的性能可能非常接近。

6. 代码维护性

在追求性能和保持代码可读性之间找平衡很重要:

1// 可能性能稍好,但可读性较差
2vec.emplace_back(std::piecewise_construct, 
3                 std::forward_as_tuple(1), 
4                 std::forward_as_tuple("complex"));
5
6// 性能可能稍差,但更易读和维护
7vec.push_back(std::make_pair(1, std::string("complex")));

7. 结论

选择push_back还是emplace_back取决于多个因素,包括对象类型、构造复杂度、代码可读性和具体的性能需求。emplace_back 经常被误认为比 push_back 更好,或者与移动语义相关,但这是错误的。在大多数情况下,选择最清晰、最直观的方法通常是最好的做法。

建议在日常使用中优先选择push_back 只有在需要emplace_back的特定功能时(例如,当处理deque<mutex>或其他不可移动的类型时),或者在性能确实成为问题时,才考虑使用emplace_back

8. 参考链接

[1] https://stackoverflow.com/questions/4303513/push-back-vs-emplace-back

[2] https://quuxplusone.github.io/blog/2021/03/03/push-back-emplace-back/