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

C++---emplace_back与push_back

在C++的标准库容器操作里,push_backemplace_back是向容器尾部添加元素时常用的两个方法。

基本概念阐释

push_back方法解析

push_back是C++标准库容器(像vectordequelist等)提供的经典方法,其功能是把一个已存在的对象添加到容器的末尾。从本质上来说,这是一种“拷贝”操作。当调用push_back时,会发生以下步骤:
首先,调用对象的拷贝构造函数或者移动构造函数。如果传入的是左值,就会调用拷贝构造函数;若传入的是右值,则调用移动构造函数。
其次,容器会分配内存来存放这个对象的副本。
最后,将对象的副本放置到容器的尾部。

下面通过一个简单的示例来直观地了解push_back的用法:

#include <vector>
#include <string>
#include <iostream>class MyClass {
public:MyClass(int value) : data(value) {std::cout << "构造函数被调用,值为: " << data << std::endl;}MyClass(const MyClass& other) : data(other.data) {std::cout << "拷贝构造函数被调用,值为: " << data << std::endl;}MyClass(MyClass&& other) noexcept : data(other.data) {std::cout << "移动构造函数被调用,值为: " << data << std::endl;}private:int data;
};int main() {std::vector<MyClass> vec;// 用左值调用push_backMyClass obj(42);vec.push_back(obj); // 调用拷贝构造函数// 用右值调用push_backvec.push_back(MyClass(100)); // 调用移动构造函数return 0;
}
emplace_back方法解析

emplace_back是C++11引入的新方法,它借助完美转发和原位构造技术,直接在容器的内存空间中构造对象,无需进行拷贝或者移动操作。调用emplace_back时,会发生以下步骤:
首先,通过完美转发将参数传递给对象的构造函数。
其次,容器在已分配好的内存位置上直接构造对象。
最后,完成对象的构造后,容器的大小会相应增加。

下面来看一个使用emplace_back的示例:

#include <vector>
#include <string>
#include <iostream>class MyClass {
public:MyClass(int value) : data(value) {std::cout << "构造函数被调用,值为: " << data << std::endl;}MyClass(const MyClass& other) : data(other.data) {std::cout << "拷贝构造函数被调用,值为: " << data << std::endl;}MyClass(MyClass&& other) noexcept : data(other.data) {std::cout << "移动构造函数被调用,值为: " << data << std::endl;}private:int data;
};int main() {std::vector<MyClass> vec;// 使用emplace_back直接构造对象vec.emplace_back(42); // 只调用一次构造函数return 0;
}

核心差异分析

构造方式的不同

push_back要求传入的是一个完整的对象,不管是左值还是右值。而emplace_back则允许传入构造对象所需的参数,这些参数会被完美转发到对象的构造函数。
来看一个对比示例:

#include <vector>
#include <string>class Person {
public:Person(std::string name, int age) : name(std::move(name)), age(age) {}private:std::string name;int age;
};int main() {std::vector<Person> people;// 使用push_back,必须显式创建Person对象people.push_back(Person("Alice", 30)); // 需要先构造一个临时对象// 使用emplace_back,可以直接传递构造参数people.emplace_back("Bob", 25); // 直接在容器内存中构造对象return 0;
}
性能表现的差异

在性能方面,emplace_back通常具有优势,特别是对于构造代价较高的对象。这是因为它避免了拷贝或者移动操作。不过,这种性能提升并不是绝对的,具体情况还需要结合实际场景来分析。
下面通过一个性能测试示例来比较两者的差异:

#include <vector>
#include <string>
#include <chrono>
#include <iostream>class ExpensiveObject {
public:ExpensiveObject(std::string data) : data(std::move(data)) {// 模拟一个耗时的操作for (int i = 0; i < 1000; ++i) {// 一些耗时的计算}}private:std::string data;
};int main() {const int N = 10000;// 测试push_back的性能auto start1 = std::chrono::high_resolution_clock::now();std::vector<ExpensiveObject> vec1;for (int i = 0; i < N; ++i) {vec1.push_back(ExpensiveObject("data"));}auto end1 = std::chrono::high_resolution_clock::now();auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();// 测试emplace_back的性能auto start2 = std::chrono::high_resolution_clock::now();std::vector<ExpensiveObject> vec2;for (int i = 0; i < N; ++i) {vec2.emplace_back("data");}auto end2 = std::chrono::high_resolution_clock::now();auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();std::cout << "push_back耗时: " << duration1 << " 毫秒" << std::endl;std::cout << "emplace_back耗时: " << duration2 << " 毫秒" << std::endl;std::cout << "性能提升: " << (100.0 * (duration1 - duration2) / duration1) << "%" << std::endl;return 0;
}
适用场景的差异

push_back适用于以下场景:

  • 需要将已存在的对象添加到容器中。
  • 代码需要兼容旧版本的C++(C++11之前)。

emplace_back则适用于以下场景:

  • 需要直接在容器中构造对象,避免拷贝或移动操作。
  • 要添加的对象构造参数较为复杂,使用emplace_back可以使代码更加简洁。
  • 对性能有较高的要求,尤其是在处理大量数据或者构造代价较高的对象时。

深入技术细节

完美转发的实现原理

emplace_back之所以能够实现高效的原位构造,关键在于它运用了完美转发技术。完美转发是C++11引入的一种机制,通过引用折叠和模板参数推导,能够将参数以原始的左值或右值属性传递给目标函数。
下面来看看完美转发的实现示例:

template<typename... Args>
void emplace_back(Args&&... args) {// 分配内存void* p = allocate_memory(sizeof(value_type));// 在分配的内存上构造对象new (p) value_type(std::forward<Args>(args)...);// 更新容器的大小++size_;
}
原位构造的优势

原位构造不仅能够避免拷贝或移动操作,还能解决一些push_back无法处理的情况。例如,当对象的构造函数被声明为explicit(显式)时,push_back在创建临时对象时可能会遇到问题,而emplace_back则可以直接传递参数,避免了这个问题。
来看一个示例:

#include <vector>
#include <string>class MyClass {
public:explicit MyClass(int value) : data(value) {}private:int data;
};int main() {std::vector<MyClass> vec;// 错误:无法隐式转换为MyClass// vec.push_back(42); // 正确:直接传递构造参数vec.emplace_back(42);return 0;
}
异常安全性考量

在异常安全性方面,emplace_backpush_back遵循相同的原则。如果对象的构造函数抛出异常,容器必须保证不会发生内存泄漏,并且状态保持不变。对于emplace_back来说,由于是原位构造,如果构造过程中抛出异常,容器的状态不会被改变。

实际应用建议

优先使用emplace_back

在大多数情况下,特别是在性能敏感的应用中,建议优先使用emplace_back。因为它能够避免不必要的拷贝或移动操作,提升代码的执行效率

注意参数类型

当传递的参数类型与容器元素类型完全匹配时,push_backemplace_back的性能差异通常不大。在这种情况下,可以根据代码的可读性来选择使用哪种方法。
例如:

std::vector<std::string> strings;// 两种方法性能相近
strings.push_back("hello");
strings.emplace_back("hello");
处理不可移动对象

对于那些不可拷贝且不可移动的对象,emplace_back是唯一可行的添加方式。因为它直接在容器的内存中构造对象,不需要进行拷贝或移动操作。
示例如下:

#include <vector>
#include <memory>int main() {std::vector<std::unique_ptr<int>> vec;// 错误:unique_ptr不可拷贝// vec.push_back(std::unique_ptr<int>(new int(42)));// 正确:使用emplace_back原位构造vec.emplace_back(new int(42));return 0;
}
性能测试与代码审查

在关键代码部分,建议进行性能测试,比较push_backemplace_back的实际表现。同时,在代码审查时,要重点关注那些构造代价较高的对象,确保使用了最有效的添加方法。

特殊情况与注意事项

容器扩容的影响

当容器需要扩容时,不管是使用push_back还是emplace_back,都可能会引发元素的移动或拷贝操作。因此,为了减少这种开销,可以预先使用reserve方法来分配足够的内存。
示例:

std::vector<int> vec;
vec.reserve(1000); // 预先分配足够的内存for (int i = 0; i < 1000; ++i) {vec.emplace_back(i); // 避免多次扩容
}
构造函数的重载

如果类的构造函数存在重载,emplace_back可能会调用与预期不同的构造函数。在这种情况下,需要特别注意参数的类型和数量,确保调用的是正确的构造函数。
示例:

#include <vector>
#include <string>class MyClass {
public:MyClass(int value) : data(value) {}MyClass(const char* str) : data(std::string(str).size()) {}private:int data;
};int main() {std::vector<MyClass> vec;// 可能不是预期的调用vec.emplace_back("hello"); // 调用MyClass(const char*)return 0;
}
兼容性问题

需要注意的是,emplace_back是C++11引入的特性,如果代码需要兼容旧版本的C++,则只能使用push_back

总结

push_backemplace_back虽然都用于向容器尾部添加元素,但它们在实现机制、性能表现和适用场景上存在明显的差异。emplace_back凭借完美转发和原位构造技术,能够避免不必要的拷贝或移动操作,通常具有更好的性能,特别是在处理构造代价较高的对象时。因此,在现代C++编程中,建议优先考虑使用emplace_back,但在代码需要兼容旧版本C++或者参数类型与容器元素类型完全匹配时,push_back仍然是合适的选择。在实际开发中,要根据具体的应用场景合理选择使用这两种方法,并结合性能测试来验证代码的效率。

http://www.lryc.cn/news/590302.html

相关文章:

  • Java接口:小白如何初步认识Java接口?
  • C语言 个人总结1
  • 【SF顺丰】顺丰开放平台API对接(Java对接篇)
  • AI Agent开发学习系列 - langchain之LCEL(2):LCEL 链式表达解析
  • Nand2Tetris(计算机系统要素)学习笔记 Project 0
  • 单片机学习笔记.IIC通信协议(根据数据手册写IIC驱动程序,这里以普中开发板上的AT24C02为例)
  • 【深度学习基础】PyTorch中model.eval()与with torch.no_grad()以及detach的区别与联系?
  • 嵌入式学习-PyTorch(5)-day22
  • 人工智能时代下的数据新职业:新兴工作岗位版图研究
  • 智能体架构深度解构:一次用户请求的完整旅程
  • 第二十一 篇 PDF文档自动化:Python一键合并、分割、水印、提取与加密解密!你的PDF全能管家!
  • audiorecord 之 抢占优先级
  • rLLM:用于LLM Agent RL后训练的创新框架
  • ESP32 S3 基于 Arduino 实现局域网视频流传输全解析
  • Python从入门到高手9.2节-Python字典的操作方法
  • 多维动态规划题解——不同路径【LeetCode】记忆化搜索
  • NumPy 常用操作详解汇总和实战示例
  • 泰语OCR识别技术方案
  • 【React Native】安装配置 Expo Router
  • STM32 ODR
  • obsidian1.8.10_win中文_Markdown编辑器_安装教程
  • 逆功率检测设备防逆流解决方案守护电网安全
  • 第五章 管道工程 5.4 管道安全质量控制
  • Uniswap V2/V3/V4简短说明
  • 功能测试和回归测试
  • 架构设计之计算高性能——单体服务器高性能
  • 更灵活方便的初始化、清除方法——fixture【pytest】
  • 使用Node搭建一个直播服务器,实时直播当前桌面
  • 获取印度股票数据API实例:NSE与BSE双市场对接指南
  • Python类中魔术方法(Magic Methods)完全指南:从入门到精通