当前位置: 首页 > news >正文

C++ STL 容器的一个重要成员函数——`emplace_back`

我们将对 C++ STL 容器的一个重要成员函数——emplace_back 进行一次全面深入的解析。

<摘要>

emplace_back 是 C++11 中为序列容器(如 std::vector, std::deque, std::list)引入的成员函数,用于在容器尾部直接构造一个新元素。其核心优势在于它接受构造参数而非完整的对象,从而允许在容器内部的内存空间中直接创建对象,避免了临时对象的创建、移动或拷贝操作。这种“就地构造”(Emplacement)技术是相比传统 push_back 方法的重大性能优化,尤其对于构造成本高(包含动态内存分配、非平凡拷贝等)或不可拷贝/移动的对象类型至关重要。它是现代 C++ 中编写高效、简洁代码的关键工具之一。


<解析>

1) 函数的概念与用途

1.1 核心概念:就地构造 (Emplacement)
传统的 push_back 方法的工作流程是:

  1. 在容器外部构造一个临时对象(或接收一个已存在的对象)。
  2. 将这个临时对象移动或拷贝到容器尾部新分配的内存中。
  3. 销毁临时对象。

emplace_back 的工作流程是:

  1. 在容器尾部新分配的内存中,直接使用提供的参数调用构造函数来创建对象。

这省去了中间步骤,实现了“零拷贝”或“零移动”的构造(当然,构造函数内部的逻辑依然存在)。

1.2 详细用途与场景
emplace_back 在以下场景中表现出巨大价值:

  1. 构造成本高的对象

    • 例如,std::vector<std::string>。使用 push_back 可能会先构造一个临时 std::string,再移动到容器中。而 emplace_back 直接根据字符数组在容器内构造 std::string,避免了移动操作。
  2. 不可移动或不可拷贝的对象

    • 如果一个类的拷贝构造函数和移动构造函数被定义为 = delete,那么它无法通过 push_back 添加到容器中。但只要它的某个构造函数可以与 emplace_back 提供的参数匹配,就可以直接构造到容器里。
  3. 容器中存储智能指针

    • 例如,std::vector<std::unique_ptr<MyClass>>unique_ptr 无法被拷贝,移动也有些繁琐。emplace_back 可以直接在容器中构造 unique_ptr,非常优雅。
  4. 需要显式调用特定构造函数的对象

    • 如果一个类有多个构造函数,而你想调用的不是默认或拷贝构造函数,emplace_back 可以直接传递参数对应该构造函数,无需先创建一个命名对象。
2) 函数的声明与出处

emplace_back 是模板成员函数,定义在各个序列容器的类定义中,如 <vector>, <deque>, <list>

std::vector 中的典型声明:

template< class... Args >
reference emplace_back( Args&&... args ); // (since C++17 返回引用)
// 在 C++11 和 C++14 中,返回类型是 void
3) 返回值的含义与取值范围
  • C++17 之前:返回 void。不返回任何值。
  • C++17 及之后:返回一个引用reference),这个引用指向在容器中被新构造出来的元素。
    • 这个返回值非常重要,它允许你链式调用(如修改新元素)或直接使用新元素,而无需通过容器的 back() 方法来查找,既方便又可能更高效。
4) 参数的含义与取值范围

emplace_back 接受一个可变参数模板包 Args&&... args

  1. Args&&... args
    • 作用:传递给新元素构造函数的参数列表。
    • 类型万能引用(Universal References)。这意味着参数会按照完美转发(Perfect Forwarding) 的规则被传递到元素的构造函数中。这保证了参数的值类别(左值/右值)被保留,从而能够调用最合适的构造函数(拷贝或移动)。
    • 取值范围:任何能够匹配到元素类型某个构造函数的参数组合。参数数量可以从 0 到多个。
5) 函数使用案例

以下提供三个典型的使用示例,均包含 main 函数并可编译运行。

示例 1:基础用法 - 构造简单对象
此示例展示 emplace_backpush_back 的基本区别。

#include <iostream>
#include <vector>
#include <string>class MyClass {
public:int a;std::string b;// 一个需要多个参数的构造函数MyClass(int x, std::string y) : a(x), b(std::move(y)) {std::cout << "Constructor called: " << a << ", " << b << std::endl;}// 拷贝构造函数MyClass(const MyClass& other) : a(other.a), b(other.b) {std::cout << "Copy Constructor called: " << a << ", " << b << std::endl;}// 移动构造函数MyClass(MyClass&& other) noexcept : a(other.a), b(std::move(other.b)) {std::cout << "Move Constructor called: " << a << ", " << b << std::endl;}~MyClass() {std::cout << "Destructor called: " << a << ", " << b << std::endl;}
};int main() {std::vector<MyClass> vec;vec.reserve(10); // 预留空间,避免重新分配干扰输出std::cout << "\n--- Using push_back with temporary ---" << std::endl;// 会先构造临时对象,再移动(或拷贝)到vector中vec.push_back(MyClass(1, "Hello"));std::cout << "\n--- Using push_back with lvalue ---" << std::endl;MyClass obj(2, "World");// 会调用拷贝构造函数vec.push_back(obj);std::cout << "\n--- Using emplace_back ---" << std::endl;// 直接使用参数在vector内部构造,没有临时对象,没有移动!vec.emplace_back(3, "C++");std::cout << "\n--- Using emplace_back with lvalue ---" << std::endl;std::string str = "Emplacement";int x = 4;// str 是左值,会调用拷贝构造string// x 是左值,会拷贝值vec.emplace_back(x, str);std::cout << "\n--- Using emplace_back with rvalue ---" << std::endl;// std::move(str) 是右值,会调用移动构造stringvec.emplace_back(5, std::move(str));std::cout << "\n--- End of scope ---" << std::endl;// 析构所有对象return 0;
}

编译与运行 (C++11 或更高):

g++ -std=c++11 -o emplace_basic emplace_basic.cpp
./emplace_basic

执行结果说明:
输出将清晰展示函数调用的区别:

  • push_back(MyClass(1, "Hello")):先调用普通构造创建临时对象,再调用移动构造将其移入容器,最后析构临时对象。
  • push_back(obj):直接调用拷贝构造。
  • emplace_back(3, "C++")只调用一次普通构造,直接在容器内完成。这是最有效的方式。
  • emplace_back(x, str):只调用一次普通构造,但参数是左值,所以内部的 std::string 是拷贝构造的。
  • emplace_back(5, std::move(str)):只调用一次普通构造,但参数 str 是右值,所以内部的 std::string 是移动构造的。

示例 2:不可拷贝/移动的对象
此示例展示 emplace_back 如何使不可能变为可能。

#include <iostream>
#include <vector>
#include <memory>class NonCopyableMovable {
public:int unique_id;explicit NonCopyableMovable(int id) : unique_id(id) {std::cout << "Constructing NonCopyableMovable " << unique_id << std::endl;}// 删除拷贝构造和拷贝赋值NonCopyableMovable(const NonCopyableMovable&) = delete;NonCopyableMovable& operator=(const NonCopyableMovable&) = delete;// 删除移动构造和移动赋值 (C++11)NonCopyableMovable(NonCopyableMovable&&) = delete;NonCopyableMovable& operator=(NonCopyableMovable&&) = delete;~NonCopyableMovable() {std::cout << "Destructing NonCopyableMovable " << unique_id << std::endl;}
};int main() {std::vector<NonCopyableMovable> vec;// 错误!无法通过 push_back 添加,因为无法移动或拷贝临时对象// vec.push_back(NonCopyableMovable(1));// 正确!使用 emplace_back 直接在向量内部构造对象vec.emplace_back(1); // 只调用一次构造函数vec.emplace_back(2);vec.emplace_back(3);std::cout << "Vector size: " << vec.size() << std::endl;for (const auto& obj : vec) {std::cout << "Object ID: " << obj.unique_id << std::endl;}std::cout << "--- End of scope ---" << std::endl;return 0;
}

编译与运行:

g++ -std=c++11 -o emplace_nocopy emplace_nocopy.cpp
./emplace_nocopy

执行结果说明:
程序成功编译并运行。输出显示只调用了构造函数和析构函数,证明了 emplace_back 无需拷贝或移动即可将 NonCopyableMovable 对象放入容器中。如果尝试使用 push_back,编译器会报错。

示例 3:存储智能指针并使用 C++17 返回值
此示例展示 emplace_back 在智能指针容器中的优雅用法,以及利用 C++17 的返回值。

#include <iostream>
#include <vector>
#include <memory>
#include <string>struct Employee {int id;std::string name;std::string department;Employee(int id, std::string name, std::string dept): id(id), name(std::move(name)), department(std::move(dept)) {}
};int main() {std::vector<std::unique_ptr<Employee>> employees;// 使用 emplace_back 构造 unique_ptr<Employee>// 参数是传递给 Employee 构造函数的employees.emplace_back(std::make_unique<Employee>(1, "Alice", "Engineering"));// 更直接的方式:employees.emplace_back(new Employee(2, "Bob", "Marketing"));// C++17: 使用返回值直接修改新添加的元素auto& new_hire = employees.emplace_back(new Employee(3, "Charlie", "Sales"));new_hire->department = "Business Development"; // 直接修改// 遍历输出for (const auto& emp : employees) {std::cout << "ID: " << emp->id<< ", Name: " << emp->name<< ", Dept: " << emp->department << std::endl;}// 错误!unique_ptr 不能拷贝// employees.push_back(std::make_unique<Employee>(4, "David", "HR"));return 0;
}

编译与运行 (需要 C++17 支持返回值):

g++ -std=c++17 -o emplace_smartptr emplace_smartptr.cpp
./emplace_smartptr

执行结果说明:
程序展示了如何轻松地管理 unique_ptr 的容器。emplace_back 直接接管了 new 返回的原始指针,将其构造为 unique_ptr 并存入容器。利用 C++17 的返回值,我们可以立即获取并修改新添加的元素,非常方便。同样,push_back 在这里无法使用。

6) 编译方式与注意事项

6.1 编译命令
使用 emplace_back 需要 C++11 或更高标准。

g++ -std=c++11 -o your_program your_source.cpp
# 如需使用返回值,需 C++17
g++ -std=c++17 -o your_program your_source.cpp

6.2 至关重要的注意事项

  1. 参数转发与显式构造函数:由于 emplace_back 使用完美转发,如果元素的构造函数是 explicit 的,你必须提供精确匹配的参数类型,不能依赖隐式转换。

    struct X { explicit X(int) {} };
    std::vector<X> v;
    // v.emplace_back(5);    // OK: 直接匹配
    // v.emplace_back('a');  // 可能编译错误:不能隐式转换 char -> int
    
  2. ** vector 重新分配**:和 push_back 一样,emplace_back 可能导致 vector 的容量不足,从而触发重新分配。重新分配的过程是:分配新内存 -> 将旧元素移动或拷贝到新内存 -> 销毁旧元素。这意味着即使你用了 emplace_back,重新分配本身也可能引发大量的移动操作。使用 reserve() 预先分配足够空间可以避免这个问题。

  3. 异常安全emplace_back 提供了强异常安全保证。如果在构造新元素时抛出异常,容器保持不变,没有元素被添加进去。但是,如果元素的构造函数不会抛出异常,那么 emplace_back 的性能优势会更加明显。

  4. 与 initializer_list 的歧义:有时,emplace_back 的行为可能出乎意料。

    std::vector<std::vector<int>> v;
    v.emplace_back(3, 4); // 你想做什么?
    // 这会调用 vector<int> 的构造函数:vector( size_type count, const T& value = T() )
    // 结果是创建一个包含 3 个 4 的向量:{4, 4, 4}
    // 如果你想添加一个列表 {3, 4},应该使用 push_back({3, 4})
    

    在这种情况下,push_back 配合初始化列表可能更直观。

  5. 性能并非总是更优:对于内置类型(如 int, double)或非常简单的、平凡的类,emplace_backpush_back 的性能差异可能微乎其微,甚至没有。编译器优化可能会使两者生成的代码完全相同。但对于复杂对象,优势是明显的。

  6. 可读性:有时 push_back 的意图更加清晰明了。在选择使用 emplace_back 时,也要考虑代码的可读性。push_back(obj) 明确表示“添加这个对象”,而 emplace_back(args...) 表示“用这些参数在末尾构造一个对象”。

7) 执行结果说明

上述三个示例的执行结果已经分别在其后进行了说明。它们共同印证了 emplace_back 的核心价值:

  • 性能提升:避免临时对象的创建和转移操作。
  • 功能增强:使存储不可拷贝/移动的对象成为可能。
  • 代码简洁:特别是与智能指针和复杂构造函数配合时。
  • 现代C++:完美利用可变参数模板、完美转发、移动语义等现代C++特性。
8) 图文总结:emplace_back vs push_back 工作机制对比
emplace_back 工作流程
在 vector 尾部分配新内存
直接在新内存中构造对象
使用提供的参数调用构造函数
push_back 工作流程
在外部内存中构造临时对象
调用构造函数
在 vector 尾部分配新内存
将临时对象移动或拷贝
到新内存中
调用移动/拷贝构造函数
销毁临时对象
调用析构函数
调用 vec.push_back(MyClass(arg))
调用 vec.emplace_back(arg)
完成添加
(多步操作,可能有开销)
完成添加
(单步操作,高效)

底层机制深度解析:

  1. 完美转发 (Perfect Forwarding)emplace_back 的实现核心是 std::forward<Args>(args)...。这保证了如果传入的是一个右值(如 42std::move(str)),那么元素的构造函数接收到就是一个右值引用,从而可以调用移动构造函数(如果存在);如果传入的是左值,则调用拷贝构造函数。这是实现高效“就地构造”的关键。

  2. 可变参数模板 (Variadic Templates)template <class... Args> 允许 emplace_back 接受任意数量、任意类型的参数,只要它们能匹配元素类型的某个构造函数。这提供了极大的灵活性。

  3. 内存管理emplace_backpush_back 在内存分配策略上完全一致。它们都需要检查 size() 是否等于 capacity(),并在需要时重新分配。真正的区别在于对象构造发生的位置和方式。

通过以上详细解析,我们可以看到 emplace_back 不仅仅是 push_back 的一个简单替代品,它是 C++11 语言特性(移动语义、完美转发、可变参数模板)与标准库容器深度融合的成果,代表了现代 C++ 对性能和表达力的不懈追求。在大多数情况下,它应该是你向序列容器尾部添加新元素的首选方法。


文章转载自:

http://xiFWdRpS.prznc.cn
http://zlBoZpQi.prznc.cn
http://B6mwAQrW.prznc.cn
http://Dm0FrZ3H.prznc.cn
http://htCNCSfi.prznc.cn
http://8lwMMctl.prznc.cn
http://elprMMIu.prznc.cn
http://acH1DRFF.prznc.cn
http://wV7LYZnw.prznc.cn
http://JQCgeBpN.prznc.cn
http://r3Q9fJzA.prznc.cn
http://KkZg9Dxv.prznc.cn
http://EBMXbQF9.prznc.cn
http://vYkbLaFo.prznc.cn
http://kf8I7a14.prznc.cn
http://rTCTQTrn.prznc.cn
http://utjGo3XV.prznc.cn
http://WSDK98qQ.prznc.cn
http://4pMnDCXy.prznc.cn
http://RzkoQUav.prznc.cn
http://sv0p5oYU.prznc.cn
http://DXHUVU4j.prznc.cn
http://fL2lGl2n.prznc.cn
http://ZaPT01QY.prznc.cn
http://JQwAOSvc.prznc.cn
http://0nVTyxGn.prznc.cn
http://p210WjsG.prznc.cn
http://xTnaSQCV.prznc.cn
http://5GZsCmuR.prznc.cn
http://qwC1uIeZ.prznc.cn
http://www.dtcms.com/a/377532.html

相关文章:

  • vue3:触发自动el-input输入框焦点
  • python range函数练习题
  • Q2(门座式)起重机司机的理论知识考试考哪些内容?
  • 企业微信消息推送
  • 顺序表:数据结构中的基础线性存储结构
  • 什么是X11转发?
  • OpenCV计算机视觉实战(24)——目标追踪算法
  • 4.2 I2C通信协议
  • Spring Boot 读取 YAML 配置文件
  • 【系统分析师】第20章-关键技术:微服务系统分析与设计(核心总结)
  • SAP-MM:SAP MM模块精髓:仓储地点(Storage Location)完全指南图文详解
  • Shell脚本周考习题及答案
  • 广东省省考备考(第九十六天9.10)——言语(刷题巩固第二节课)
  • Pthread定时锁与读写锁详解
  • Go模块自动导入教学文档
  • 技术文章大纲:开学季干货——知识梳理与经验分享
  • TensorFlow平台介绍
  • Vue3 中实现按钮级权限控制的最佳实践:从指令到组件的完整方案
  • 生成模型与概率分布基础
  • Cookie之domain
  • JavaSSM框架-MyBatis 框架(五)
  • 中州养老:设备管理介绍
  • 【Day 51|52 】Linux-tomcat
  • MySQL - 如果没有事务还要锁吗?
  • “高德点评”上线,阿里再战本地生活
  • JUC的常见类、多线程环境使用集合类
  • 《C++ 108好库》之1 chrono时间库和ctime库
  • C++篇(7)string类的模拟实现
  • 弱加密危害与修复方案详解
  • 【Linux】Linux常用指令合集