【C++11(中)】—— 我与C++的不解之缘(三十一)
2026/6/1 7:23:08 网站建设 项目流程

一、可变参数模版

基本语法:

C++11支持可变参数模版,简单来说就是支持可变数量参数的函数模版或者类模版;

可变数目的参数被称为参数包,存在两种参数包:模版参数包(表示0个或者多个模版参数),函数参数包(表示0个或者多个函数参数)。

template<class...Args>voidfunc(Args...args){}template<class...Args>voidfunc(Args&...args){}template<class...Args>voidfunc(Args&&...args){}

  • 我们使用...来指出一个模版参数或者函数参数,表示一个参数包;
  • 在模版参数列表在,class...或者typename...指出接下来的参数表示0个或者多个类型列表;
  • 在函数参数列表中,类型名后跟...指出接下来表示0个或者多个形参对象列表;
  • 函数参数包可以使用左值引用右指引用表示,每一个参数实例化时依然遵循引用折叠的规则。

这里我们可以使用sizeof...()来计算参数包里面有多少个参数。

template<class...Args>voidfunc(Args...args){cout<<sizeof...(args)<<endl;}intmain(){func();//0个参数func(1);//1个参数func(1,"love");//2个参数func(1,"love",1.1);//3个参数return0;}

原理:

这里可变模版参数的原理和模版类似,本质上还是编译器去实例化对应类型和个数的多个函数。

就以上述代码来说,编译器实际上是根据我们传参的类型实例化出来了多个函数:

voidfunc();voidfunc(int&&a);voidfunc(int&&a,string&&b);voidfunc(int&&a,string&&b,doublec);

这里我们如果实现下列普通函数模版,也能到达目的:

voidfunc();template<classT1>voidfunc(T1&&a);template<classT1,classT2>voidfunc(T1&&a,T2&&b);template<classT1,classT2,classT3>voidfunc(T1&&a,T2&&b,T3&&c);

但是这样未必有些太麻烦了,如果我们还要传递4、5、6个甚至更多参数的,那还要一个个去实现。

而有了可变参数模板,我们只需要实现一个,就可以达到普通函数模板多个的效果。

**理解:**这里我们可以简单的理解成,可变参数函数模板先实例化出多个普通函数模板,在这进一步实例化出具体类型的函数。

当然编译器并不会这样去做,而是直接实例化出参数类型个个数对应的函数。

包扩展:

  • 对于一个参数包,我们能够使用sizeof...()去计算它的个数,除此之外,唯一能做的就是扩展它了。

那如何扩展呢?

  1. 当扩展一个参数包时,我们还需要提供一个用来扩展每一个元素的模式(扩展包简单来说就是将包中元素一个个取出来),这里对每一个元素应用模式,获得扩展之后的列表。
  2. 通过在模版的右边放一个...来触发扩展操作。
voidShowList(){//编译器递归推导,当参数包中参数个数为0时匹配cout<<endl;}template<classT,class...Args>voidShowList(T x,Args...args){cout<<x<<" ";//args参数包中参数的个数为N//调用ShowList,参数包的第一个参数传给x,剩下的N-1个参数传给第二个参数包ShowList(args...);}template<class...Args>voidfunc(Args...args){ShowList(args);}intmain(){func();//0个参数func(1);//1个参数func(1,"love");//2个参数func(1,"love",1.1);//3个参数return0;}

这里,也可以这样去扩展包

template<classT>constT&GetArg(constT&x){cout<<x<<" ";returnx;}template<class...Args>voidArguments(Args...args){}template<class...Args>voidfunc(Args...args){// 注意GetArg必须返回获得到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);}

这里本质上,编译器将上述模版函数扩展实例化为下面这样

voidfunc(intx,string y,doublez){Arguments(GetArg(x),GetArg(y),GetArg(z));}

包扩展这里简单了解一下就OK了好吧(很少去自己实现或者使用包扩展)

在更多的情况下,是直接将包向下传递,直接匹配参数列表。

二、emplace系列

template<class...Args>voidemplace_back(Args&&...args);template<class...Args>iteratoremplace(const_iterator position,Args&&...args);

C++11之后,STL容器新增了emplace系列的接口,emplace系列的接口都是可变参数模版,功能上和pushinsert一样;

但是emplace也支持一些新的东西,就比如对于容器container<T>emplace还支持直接插入构造T对象的参数,一些情况下会更加高效(在容器中直接构造T类型对象,而不是构造临时对象再进行拷贝构造/移动构造

intmain(){list<HL::string>lt1;HL::strings1("111111111111");cout<<"------------------------------------"<<endl;//左值,调用拷贝构造lt1.push_back(s1);cout<<"------------------------------------"<<endl;//右值,调用移动构造lt1.push_back(move(s1));cout<<"------------------------------------"<<endl;//push_back(),先创建临时对象,再调用移动构造lt1.push_back("111111111111");cout<<"------------------------------------"<<endl<<endl;list<HL::string>lt2;HL::strings2("111111111111");cout<<"------------------------------------"<<endl;//左值,调用拷贝构造lt2.emplace_back(s2);cout<<"------------------------------------"<<endl;//右值,调用移动构造lt2.emplace_back(move(s2));cout<<"------------------------------------"<<endl;//emplace_back(),直接构造lt2.emplace_back("111111111111");cout<<"------------------------------------"<<endl<<endl;return0;}

这里可以看到,push_back()是先构造了临时对象,再进行移动构造;

emplace_back()则是直接调用了构造函数。

这里push_back()是普通函数,在类模版实例化是时候,参数类型就已经确定了,上述代码中是string;而我们能够使用"111111111111"作为参数的原因就是:单参数的构造函数支持隐式类型转换。

emplace_back()是可变参数函数模版,在类模版实例化时它不会被实例化,所以emplace_back(111111111111"),它接受的参数类型是const char*,它就会将参数列表继续向下传递,直到参数类型是const char*的构造函数。

这里使用上篇文章中自己实现的string类(可以去看一下上篇文章的string类,其中构造函数参数是const char*(输出了strinf(char* str)-构造)。

这里了解了emplace,现在我们来对之前实现过的list,增加上emplace_back()emplace()接口

这里就只展示新增的代码了,详细请见:【list的模拟实现】—— 我与C++的模拟实现(十四)

ListNode

template<class...Args>ListNode(Args&&...args):_next(nullptr),_prev(nullptr),_data(std::forward<Args>(args)...){}

list

template<class...Args>voidemplace_back(Args&&...args){emplace(end(),std::forward<Args>(args)...);}template<class...Args>iteratoremplace(iterator pos,Args&&...args){Node*cur=pos._node;Node*newnode=newNode(std::forward<Args>(args)...);Node*prev=cur->_prev;// prev newnode curprev->_next=newnode;newnode->_prev=prev;newnode->_next=cur;cur->_prev=newnode;returniterator(newnode);}

这样我们在调用emplace_back()时,它会将参数包继续向下传到emplace,而emplace进行构造节点时,将参数包传递给了ListNode的构造函数。

这里要注意,为了保证参数包中参数的右值属性,我们要使用完美转发

除此之外呢,可变参数函数模版emplace还可以这样来使用:

intmain(){list<pair<HL::string,int>>lt1;pair<HL::string,int>kv("苹果",1);//和push_back()一样,对于左值需要进行深拷贝,拷贝到节点中lt1.emplace_back(kv);cout<<"------------------------------------------------"<<endl;//对于右值就是移动构造lt1.emplace_back(move(kv));cout<<"------------------------------------------------"<<endl;//push_back进行插入pair类型对象时,必须使用{}括起来(先构造pair类型的临时对象)lt1.push_back({"苹果",1});cout<<"------------------------------------------------"<<endl;//emplace_back进行插入pair对象时,不能使用{},因为它是可变参数函数模版,编译器不知道{"苹果",1}要构造成什么//emplace_back()将参数包继续往下传,直到pair类型构造(参数匹配了)lt1.emplace_back("苹果",1);cout<<"------------------------------------------------"<<endl;return0;}

三、新的类功能

C++11有了左值右值等等这些概念以后,类有有了一些新的内容

默认的移动构造和移动赋值

在原来的C++类中,一共有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载;默认成员函数就是我们不写,编译器会生成一个默认的。C++11中新增了两个默认成员函数:移动构造函数和移动赋值重载

  • 如果我们没有自己实现移动构造函数,且没有实现析构函数拷贝构造拷贝赋值重载中任意一个;此时编译器会生成一个默认移动构造。(默认生成的移动构造,对于内置类型会指向逐成员按字节拷贝;对于自定义类型成员,如果该成员实现了移动构造就调用带成员的移动构造,否则就调用拷贝构造。
  • 如果我们没有自己实现移动赋值重载函数, 且没有实现析构函数拷贝构造拷贝赋值重载这任意一个,编译器就会生成一个默认移动赋值重载函数。(对于内置类型成员,完成逐字节拷贝;对于自定义类型成员,如果该成员实现了移动赋值重载,就调用移动赋值重载,否则调用拷贝赋值。
  • 如果自己实现了移动构造和移动赋值,编译器就不会生成拷贝构造和拷贝赋值了。

成员声明是给缺省值

成员变量声明时给缺省值,这个缺省值是给初始化列表使用的;

如果没有显示的在初始化列表进行初始化,就会使用这个缺省值去初始化成员变量。

defultdelete

  • C++11设定了defult,让我们更好的控制要使用的默认函数;加入我们要使用某一个编译器生成的默认函数,但是因为我们实现了其他的导致编译器没有生成(我们实现了拷贝构造,编译器就不会生成移动构造);我们可以使用defult关键字来指定要编译器生成。string(string&& str) = defult;(以string为例)。
  • 那如果我们不想要编译器默认生成某一个默认成员函数,在之前,我们可以只声明不定义并设置成私有成员private,这时就不能调用;在C++11中,我们只需要在函数声明后面加上=delete,这样就可以指明让编译器不生成某个默认成员函数。(这里也称=delete修饰的函数为删除函数

finaloverride

final的作用就是禁止类被继承禁止虚函数的重写

override的作用就是:用于显示地标注一个虚函数是对基类虚函数的重写(override)

这里不过多描述了,更多详细内容可以见【继承】—— 我与C++的不解之缘(十九)和【多态】—— 我与C++的不解之缘(二十)

STL中容器的一些变化

C++11有了这些新语法以后,STL有更新了一些新的内容:

  • 首先就是每一个容器的emplacepush/insert系列的右值引用版本、移动构造和移动赋值initializer_list这些
  • 其次就是hash版本的unordered_setunordered_map
  • 范围for语法

这些内容我们多多少少都已经了解了,这里就不过多叙述了。

STL还新增了一个容器array

这个array简单来说就是对数组的封装,这里简单了解一下就OK了。

感谢各位的支持!!!

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询