C++11 容器的emplace方法分析

经常在学习和开发中听到,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 - 右值引用icon-default.png?t=N7T8https://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++的博大精深!

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值