C++雾中风景9:emplace_back与可变长模板

C++11的版本在vector容器添加了emplace_back方法,相对于原先的push_back方法能够在一定程度上提升vector容器的表现性能。所以我们从STL源码角度来切入,看看这两种方法有什么样的区别,新引进的方法又有什么可学习参考之处。

1.emplace_back的用法

emplace_back方法最大的改进就在与可以利用类本身的构造函数直接在内存之中构建对象,而不需要调用类的拷贝构造函数与移动构造函数。

举个栗子,假设如下定义了一个时间类time,该类同时定义了拷贝构造函数与移动构造函数:

 
  1. class time {

  2. private:

  3. int hour;

  4. int minute;

  5. int second;

  6.  
  7. public:

  8. time(int h, int m, int s) :hour(h), minute(m), second(s) {

  9. }

  10.  
  11. time(const time& t) :hour(t.hour), minute(t.minute), second(t.second) {

  12. cout << "copy" << endl;

  13. }

  14.  
  15. time(const time&& t) noexcept:hour(t.hour),minute(t.minute),second(t.second) {

  16. cout << "move" << endl;

  17. }

  18. };

在main方法之中执行下面的代码逻辑:

 
  1. int main()

  2. {

  3. vector<time> tlist;

  4. time t(1, 2, 3);

  5. tlist.emplace_back(t);

  6. tlist.emplace_back(2, 3, 4); //直接调用了time的构造函数在vector的内存之中建立起新的对象

  7.  
  8. getchar();

  9. }

执行结果:

 
  1. copy

  2. move (这次拷贝构造函数的调用是因为vector本身的扩容,也就是移动之前的已经容纳的time对象)

由上述代码我们看到time对象可以直接利用emplace_back方法在vector上构造对象,通过这样的方式来减少不必要的内存操作。(省去了拷贝构造的环节)。同样的在main之中执行下面的代码逻辑:

 
  1. int main()

  2. {

  3. vector<time> tlist;

  4. time t(1, 2, 3);

  5. tlist.emplace_back(move(t)); //调用move函数使time对象成为右值,可以利用移动构造函数来拷贝对象

  6. tlist.emplace_back(2, 3, 4); //直接调用了time的构造函数在vector的内存之中建立起新的对象

  7.  
  8. getchar();

  9. }

执行结果:

 
  1. move

  2. move (这次拷贝构造函数的调用是因为vector本身的扩容,也就是移动之前的已经容纳的time对象)

通过这样的方式也减少不必要的内存操作。(省去了移动构造的环节)。所以这就是为什么在C++11之后提倡大家使用emplace_back来代替旧代码之中的push_back函数。如下面的代码所示,在push_back底层也是调用了emplace_back来实现对应的操作流程:

 
  1. void push_back(const _Ty& _Val) {

  2. emplace_back(_Val);

  3. }

  4.  
  5. void push_back(_Ty&& _Val) {

  6. emplace_back(_STD move(_Val));

  7. }

2.emplace_back的实现

源码面前,了无秘密,接下来跟随笔者直接来看看emplace_back的源代码,来引出我们今天的主题:

 
  1. public:

  2. template<class... _Valty>

  3. decltype(auto) emplace_back(_Valty&&... _Val)

  4. { // insert by perfectly forwarding into element at end, provide strong guarantee

  5. if (_Has_unused_capacity())

  6. {

  7. _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);

  8. }

  9. else

  10. { // reallocate

  11. const size_type _Oldsize = size();

  12.  
  13. if (_Oldsize == max_size())

  14. {

  15. _Xlength();

  16. }

  17.  
  18. const size_type _Newsize = _Oldsize + 1;

  19. const size_type _Newcapacity = _Calculate_growth(_Newsize);

  20. bool _Emplaced = false;

  21. const pointer _Newvec = this->_Getal().allocate(_Newcapacity);

  22. _Alty& _Al = this->_Getal();

  23.  
  24. _TRY_BEGIN

  25. _Alty_traits::construct(_Al, _Unfancy(_Newvec + _Oldsize), _STD forward<_Valty>(_Val)...);

  26. _Emplaced = true;

  27. _Umove_if_noexcept(this->_Myfirst(), this->_Mylast(), _Newvec);

  28. _CATCH_ALL

  29. if (_Emplaced)

  30. {

  31. _Alty_traits::destroy(_Al, _Unfancy(_Newvec + _Oldsize));

  32. }

  33.  
  34. _Al.deallocate(_Newvec, _Newcapacity);

  35. _RERAISE;

  36. _CATCH_END

  37.  
  38. _Change_array(_Newvec, _Newsize, _Newcapacity);

  39. }

  40.  
  41. #if _HAS_CXX17

  42. return (this->_Mylast()[-1]);

  43. #endif /* _HAS_CXX17 */

  44. }

通过上述代码可以看到,emplace_back的流程逻辑很简单。先检查vector的容量,不够的话就扩容,之后便通过**_Alty_traits::construct来创建对象。而最终利用强制类似装换的指针来指向容器类之中对应类的构造函数,并且利用可变长模板**将构造函数所需要的内容传递过去构造新的对象。

 
  1. template<class _Objty,

  2. class... _Types>

  3. static void construct(_Alloc&, _Objty * const _Ptr, _Types&&... _Args)

  4. { // construct _Objty(_Types...) at _Ptr

  5. ::new (const_cast<void *>(static_cast<const volatile void *>(_Ptr)))

  6. _Objty(_STD forward<_Types>(_Args)...);

  7. }

emplace_back这里最为巧妙的部分就是利用可变长模板实现了,任意传参的对象构造。可变长模板是C++11新引进的特性,接下来我们来详细看看可变长模板是如何来使用,来实现任意长度的参数呢?

3.可变长模板与函数式编程

首先,我们先看看,可变长模板的定义:

 
  1. template <class... T>

  2. void f(T... args);

通过template来声明参数包args,这个参数包中可以包含0到任意个参数,并且作为函数参数调用。之后我们便可以在函数之中将参数包展开成一个一个独立的参数。

假设我们有如下需求,需要定义一个max_num函数来求出一组任意参数数字的最大值,在C++11之前的版本或许需要这样去定义这个函数,也就是说我们需要一个参数来指定对应参数的个数,并且这个过程之中存在参数的类型不一致的潜在风险,并不能在编译期进行反馈(不能在编译期进行对于动态语言来说根本不是什么大不了的问题,囧rz):

 
  1. int max_num(int count, ...)

  2. {

  3. va_list ap;

  4. va_start(ap, count);

  5.  
  6. int ans = va_arg(ap, int);

  7. for (int i = 1; i < count; ++i)

  8. {

  9. int num = va_arg(ap, int);

  10. ans = max(ans, num);

  11. }

  12.  
  13. va_end(ap);

  14.  
  15. return ans;

  16. }

而利用可变长模板,我们可以很优雅地通过以下的代码来实现一个这样的函数:

 
  1. template<typename t1,typename ...t2> t1 max_num(t1 num, t2 ...args) {

  2. auto n = max_num(args...);

  3. return n > num ? n : num;

  4. }

  5. template<typename t1> t1 max_num(t1 num) {

  6. return num;

  7. }

通过不断递归的方式,提取可变长模板参数之中的首个元素,并且设置递归的终止点的方式来依次处理各个元素。这种处理函数的方式本质上就是在通过递归的方式处理列表,这种编程思路在函数式编程语言之中十分常见,在C++之中看到这样的用法,也让笔者作为C++的入门选手感到很新奇。笔者曾经接触过Scala与Erlang语言之中大量利用了这种写法,但是多层递归导致的必然是栈调用的开销变大,利用尾递归的方式来优化这样的写法,才能减少非必要的函数调用开销。

4.小结

由emplace_back引申出来不少对C++11新特性的探索,笔者也仅仅做一些抛砖引玉的工作。作为程序员,希望大家能够坚持不断动态更新对语言的学习与探索来凝练与高效率的Coding,这也是笔者坚持更新该系列文章的初衷。

转载于:https://my.oschina.net/u/4000302/blog/3080162

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值