经常在学习和开发中听到,vector要少用push_back()方法,而是用emplace_back()方法进行数据插入,会提高性能,但是具体的原理没有深入了解。今天深入了解并进行了源码剖析,在此记录下。
版本引入
push_back
函数在 C++ 标准库的早期版本中就已经存在,它是伴随着std::vector
容器一同引入的。具体是哪个版本首次提出可能难以精确追溯,但可以确定的是,在 C++98 标准中,push_back
就已经是std::vector
的一个成员函数了。
emplace_back
是在 C++11 标准中引入的。C++11 对 STL 进行了许多扩展和改进,其中就包括对std::vector
容器添加了emplace_back
方法。emplace_back
与push_back
的主要区别在于,emplace_back
能够直接在容器内部构造元素,避免了不必要的复制或移动操作,因此在某些情况下效率更高。
到底优化了什么?
写一个Test类,完善其各种构造函数和析构函数,使用STL的vector容器,使用push_back()方法和emplace_back()方法插入Test对象。通过插入不同状态的对象,观察成员函数调用的情况。
class Test {
public:
Test(int) { cout << "Test(int)" << endl; }
Test(int, int) { cout << "Test(int, int)" << endl; }
Test(const Test&) { cout << "Test(const Test&)" << endl; } //左值拷贝构造函数
Test(Test&&) { cout << "Test(Test&&)" << endl; } // 右值拷贝构造
~Test() { cout << "~Test()" << endl; }
};
版本一代码:
#include <iostream>
#include <vector>
using namespace std;
class Test {
public:
Test(int) { cout << "Test(int)" << endl; }
Test(int, int) { cout << "Test(int, int)" << endl; }
Test(const Test&) { cout << "Test(const Test&)" << endl; } //左值拷贝构造函数
Test(Test&&) { cout << "Test(Test&&)" << endl; } // 右值拷贝构造
~Test() { cout << "~Test()" << endl; }
};
int main() {
Test t(10);
vector<Test> v;
// 此时v.capacity() == 0, 容器容量为0,需手动调整,
// 防止测试过程中容器动态扩容导致重新拷贝构造对象
//cout << v.capacity() << endl;
v.reserve(100);
cout << "===================" << endl;
// 直接插入对象,两方法无区别
v.push_back(t); // Test(const Test&)
v.emplace_back(t); //Test(const Test&)
cout << "===================" << endl;
// 插入临时变量,调用右值拷贝构造对象放入容器,然后析构临时对象,两者也无区别
v.push_back(Test(10)); // Test(int) Test(Test&&) ~Test()
v.emplace_back(Test(10)); // Test(int) Test(Test&&) ~Test()
cout << "===================" << endl;
// 给emplace传入对象参数,会直接调用相应的构造函数,在容器底层构造对象
v.push_back(10); // Test(int) Test(Test&&) ~Test()
v.emplace_back(10,20); // Test(int, int)
cout << "===================" << endl;
return 0;
}
程序打印输出如下:
可以发现,只有在向两个函数中传入对象的参数的时候,才会有所区别。
push_back()函数会按照传入的参数构造一个临时对象,然后使用这个临时对象通过右值拷贝构造函数在vector中构造一个对象,然后再将临时对象析构。
然而对于emplace_back()函数,将直接在vector底层调用与参数对应的构造函数进行对象的构造,对象直接容器中生成。这样将比前者少调用一次拷贝构造和析构函数调用,减少了时间和空间的消耗。
手动实现vector的push_back和emplace_back函数
通过手动实现一个简单的vector,实现其push_back和emplace_back函数,深入理解实现原理。
其中一些关于右值引用、move语义、完美转发的难点,可以在博客进行学习:C++11 - 右值引用https://blog.csdn.net/QIANGWEIYUAN/article/details/88653747代码如下:
/*myvactor.h*/
#include <utility>
// 由于容器创建时只开辟空间,不向其中添加对象,故需要
// 重新实现容器的空间配置器
template<typename T>
struct MyAllocator
{
// allocate deallocate 负责开辟内存和释放内存
// construct destroy 在指定的内存上构造对象(new)和析构对象
T* allocate(size_t size) {
return (T*)malloc(size * sizeof(T));
}
// construct实现成可变参模板
template<typename... Types>
void construct(T* ptr, Types&&... args) {
// 使用完美类型转发,保持args的引用类型
// 若args只是一个是Test对象参数,则匹配Test左值引用拷贝构造函数
// 若args是几个参数,则匹配Test的相应有参构造函数
// args是一个Test临时对象,则匹配Test右值引用拷贝构造函数
// 在 ptr 指向的地址上构造一个 T 类型的对象
new (ptr) T(std::forward<Types>(args)...);
}
};
template<typename T, typename Alloc = MyAllocator<T>> // 用容器类型实例化Allocator
class vector
{
public:
vector() : vec_(nullptr), size_(0), idx_(0) {}
//预留内存空间,只开辟内存,不构造对象
void reserve(size_t size) {
vec_ = allocator_.allocate(size);
size_ = size;
}
// push_back 普通版本
//void push_back(const T& val) {
// allocator_.construct(vec_ + idx_, val);
// idx_++;
//}
//void push_back(T&& val) { // 右值引用,临时量移动给对象
// // 右值引用val本身是左值,直接传递val会调用T的左值引用拷贝构造
// // 使用move做资源转移,匹配T的右值引用拷贝构造
// allocator_.construct(vec_ + idx_, std::move(val));
// idx_++;
//}
// push_back 模板版本。 与emplace_back不同的是,没有模版参数包(...)
// 这是因为STL中push_back参数只有一个,源码中是Type类的引用类型
// 即使传入参数也会匹配构造函数生成临时变量之后,再构造容器中的对象
// 实参传递 左值:Type&& val => T& val 右值:Type&& val => T&& val
template<typename Type>
void push_back(Type&& val) {
// 完美转发使用原因同emplace_back
allocator_.construct(vec_ + idx_, std::forward<Types>(args)...);
idx_;
}
// emplace_back,实现成 成员方法的模版
// 1、引用折叠:实参为左值,则判断 args为左值引用,Test&+&&=Test& args
// 实参为右值,则判断 Types&& args 引用右值,完全正确
// Types&&参数可以接受左值或者右值 进行自动推导
template<typename... Types> //有多个模板类型参数
void emplace_back(Types&&... args) { // 接收不固定的多个类型的多个参数
// args... 参数列表不论是左值引用还是右值引用变量,它本身都是左值
// 这样传递给construct函数中全是左值,都会调用Test类的左值引用构造函数
// 所以传递过程中要保持args的引用类型(左值的、右值的)=》完美转发forward
allocator_.construct(vec_ + idx_, std::forward<Types>(args)...);
idx_++;
}
private:
T* vec_; //指向动态开辟的内存的指针
int size_; // 当前vector长度,本类不实现扩容相关逻辑
int idx_; // 数组下标
Alloc allocator_; // 专门给本容器进行内存开辟释放、对象构造和析构
};
以上代码文件为myvector.h,通过引用本头文件,可以实现vector的push_back和emplace_back,代码如下:
#include <iostream>
#include "myvector.h"
using namespace std;
class Test {
public:
Test(int) { cout << "Test(int)" << endl; }
Test(int, int) { cout << "Test(int, int)" << endl; }
Test(const Test&) { cout << "Test(const Test&)" << endl; } //左值拷贝构造函数
Test(Test&&) { cout << "Test(Test&&)" << endl; } // 右值拷贝构造
~Test() { cout << "~Test()" << endl; }
};
int main() {
Test t(10);
vector<Test> v;
// 此时v.capacity() == 0, 容器容量为0,需手动调整,
// 防止测试过程中容器动态扩容导致重新拷贝构造对象
//cout << v.capacity() << endl;
v.reserve(100);
cout << "===================" << endl;
// 直接插入对象,两方法无区别
v.push_back(t); // Test(const Test&)
v.emplace_back(t); //Test(const Test&)
cout << "===================" << endl;
// 插入临时变量,调用右值拷贝构造对象放入容器,然后析构临时对象,两者也无区别
v.push_back(Test(10)); // Test(int) Test(Test&&) ~Test()
v.emplace_back(Test(10)); // Test(int) Test(Test&&) ~Test()
cout << "===================" << endl;
// 给emplace传入对象参数,会直接调用相应的构造函数,在容器底层构造对象
v.push_back(10); // Test(int) Test(Test&&) ~Test()
v.emplace_back(10,20); // Test(int, int)
cout << "===================" << endl;
return 0;
}
以上运行输出与上文版本一代码一致。
总结
myvector.h代码以及注释中,展示了两个成员函数的实现方式以及要点,需要好好琢磨。
其他STL容器的相关函数实现也与其类似。比如在map中也有优化:
map<int, string> m;
m.insert(make_pair(10, "zhangsan")); // 需要构造和析构pair对象
m.emplace(10, "zhangsan"); //可以减少对象构造析构函数调用次数
完结撒花~
越往深入学,越发现C++的博大精深!