可变参数模板
基本语法和原理
- C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包: 表示零或多个模板参数; 函数参数包: 表示零或多个函数参数。
template<class...Args>voidFunc(Args...args){...//函数体}template<class...Args>voidFunc(Args&...args){...//函数体}template<class...Args>voidFunc(Args&&...args){...//函数体}- 我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,
class...或typename...指出接下来的参数表示零或多个类型列表; - 在函数参数列表中,类型名后面跟…指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板-样,每个参数实例化时遵循引用折叠规则。
template<class...Args>voidPrint(Args&&...args){cout<<sizeof...(args)<<endl;}intmain(){doublex=2.2;Print();// 包⾥有0个参数Print(1);// 包⾥有1个参数Print(1,string("xxxxx"));// 包⾥有2个参数Print(1.1,string("xxxxx"),x);// 包⾥有3个参数return0;}- 这里我们可以使用
sizeof...运算符去计算参数包中参数的个数。
实际上编译本质会根据我们传的参会和引用折叠的规则实例化四个参数
voidPrint();voidPrint(int&&arg1);voidPrint(int&&arg1,string&&arg2);voidPrint(double&&arg1,string&&arg2,double&arg3);如果这里我们没有可变参数模板, 相当与我们写了下面四个函数模板才能支持这样的功能
voidPrint();template<classT1>voidPrint(T1&&arg1);template<classT1,classT2>voidPrint(T1&&arg1,T2&&arg2);template<classT1,classT2,classT3>voidPrint(T1&&arg1,T2&&arg2,T3&&arg3);- 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
包扩展
- 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(…)来触发扩展操作。
递归展开参数包
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>voidPrint(Args...args){ShowList(args...);}intmain(){Print();Print(1);Print(1,string("xxxxx"));Print(1,string("xxxxx"),2.2);return0;}所谓"递归展开参数包",指的是编译器实例化出这一串函数的过程在编译期发生
Print<int,string,double>ShowList<int,string,double>ShowList<string,double>ShowList<double>ShowList()有两条ShowList重载,编译器在实例化时根据参数个数自动选择,形成递归终止/推进
void ShowList(); // 参数包为空时的重载template <class T, class …Args>void ShowList(T x, Args… args); // 至少有 1 个参数时的重载``
参数个数 ≥ 1 → 匹配模板版:取出第一个给 x,其余装进新包 args。
参数个数 = 0 → 模板版无法推导(带 T x 至少要一个实参),只能匹配空参数的非模板版。
展开过程
变参模板的包展开
- C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
template<classT>constT&GetArg(constT&x){cout<<x<<endl;returnx;}template<class...Arg>voidArguments(Arg...args){}template<class...Arg>voidPrint(Arg...args){Arguments(GetArg(args)...);}intmain(){Print(10,"xxxxx",3.3);return0;}推导流程
Print函数
模板参数包Arg...从实参推导:
Arg={int,constchar*,double}args={10,"xxxxx",3.3}也就是三个独立类型的参数,不是三个同类型参数。
Arguments(GetArg(args)...);函数GetArg(args)...是一个参数包展开。模式是GetArg(args),把包 args 逐个代入,展开成逗号分隔的实参列表:
Arguments(GetArg(args_0),GetArg(args_1),GetArg(args_2));// 即Arguments(GetArg(10),GetArg("xxxxx"),GetArg(3.3));在调用Arguments(GetArg(args)...);函数前, 先将GetArg()函数求值, 实际Arguments(GetArg(args)...);函数并没有具体的作用.
这时候的Arguments(GetArg(args)...);函数的参数列表仍然是int, const char*, double.
empalce 系列接口
template<class...Args>voidemplace_back(Args&&...args);template<class...Args>iteratoremplace(const_iterator position,Args&&...args);C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为
container<T>,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
emplace_back 快在哪
省掉"实参先转换成临时 T,再拷/移进容器"这一步——参数直达元素的构造函数。
对比lt.push_back("literal")(参数是字面量const char[N]):
push_backvoidpush_back(T&&x);// 形参类型已是 string- 字面量要绑到 string&&,门口先隐式转换出临时 string → string(const char*)(构造 1)
- 这个临时再被移动进
_data(构造 2) - 临时析构
emplace_backtemplate<class...Args>voidemplace_back(Args&&...args){insert(end(),std::forward<Args>(args)...);}- 字面量
const char[N]原样被转发(Args = const char(&)[N]) _data(std::forward<Args>(args)...)直接调用string(const char*)就地构造(构造 1)
省的正是第 1 步那个临时对象 + 随后的移动。 更一般地,emplace_back 还能直接接受构造参数组合,例如 emplace_back(5, ‘x’) 直接 string(5,‘x’),而 push_back 得你先手工造好这个 string。
*注意:这两个层次是同一个原理——push_back 的形参写死 T→ 逼出临时对象;
*ListNode(const T&)的形参写死 T → 同样逼出临时。
emplace_back 把整条链(entry → insert → ListNode)都改成变参转发,把这个开销在每一层都消掉了。
- emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
emplace_back可以直接插入构造T对象的原因就是使用的可变参数模板
zy::list<pair<zy::string,int>>lt1;lt1.emplace_back("苹果",1);以插入pair对象为例, emplace_back可以直接插入而push_back就不可以, push_back需要先隐式类型转换 成pair再插入
而emplace_back, 可以自动推导出参数是string和int即forward<Arg>(x)...包展开,变成forward<Arg0>(string), forward<Arg1>(int),将(“苹果”,“1”)原样传给insert.
insert再在Node* newnode = new Node(std::forward<Args>(args)...);处原样传给ListNode构造函数.
template<class...Arg>ListNode(Arg&&...args):_next(nullptr),_prev(nullptr),_data(forward<Arg>(args)...){}也就是在_data(forward<Arg>(args)...)就等同于pair<string,int>(“苹果”,“1”); 直接调用构造函数, 构造出对象插入.
新的类功能
默认移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。
C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值初始化.
defult和delete
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
lambda
lambda的应用
在学习lambda表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用lambda去定义可调用对象,既简单又方便。
lambda在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda的应用还是很广泛的。
Lambda(匿名函数)是一种无需命名、就地定义的函数。以 C++
C++(C++11+)autoadd=[](inta,intb){returna+b;};std::cout<<add(1,2);// 3intx=10;autocap=[x](inty){returnx+y;};// [x] 捕获外部变量lambda原理
lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而 lambda 底层是仿函数对象,也就是说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,
lambda 参数/返回类型/函数体就是仿函数 operator() 的参数/返回类型/函数体,lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。上面的原理,我们可以透过汇编层了解一下,下面第二段汇编层代码印证了上面的原理。
语法结构:
[捕获列表](参数列表) -> 返回类型 { 函数体 }
1. 参数列表
和普通函数一样。无参可省略()(C++11 起):
autof=[]{return42;};autog=[](inta,intb){returna+b;};2. 返回类型-> type
可省略,编译器自动推导。但多个return且类型不一致时必须显式指定:
autof=[](intx){returnx*1.0;};// 自动推导为 doubleautog=[](intx)->double{// 显式指定if(x>0)returnx;elsereturn-1.0;};[!warning] 注意
只有一条return语句时才可靠推导,否则默认推导为void(除非显式写返回类型)。(C++11 规则;C++14 起多条
return且类型相同也可自动推导。类型不一致时建议显式指定。)
3. 捕获列表[]
决定 lambda 能访问哪些外部变量,及方式(值/引用):
| 写法 | 含义 |
|---|---|
[] | 不捕获任何外部变量 |
[x] | 按值捕获x(拷贝一份,默认const) |
[&x] | 按引用捕获x |
[=] | 按值捕获所有用到的外部变量 |
[&] | 按引用捕获所有用到的外部变量 |
[=, &x] | 默认按值,x按引用 |
[&, x] | 默认按引用,x按值 |
[this] | 捕获当前对象的this指针 |