一、可变模板参数
1.1 概念
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
1.2 递归函数方式展开参数包
// 递归终止函数,当只有一个参数时特殊处理
template<class T, class ...Args>
void ShowList(T value)
{
cout << typeid(value).name() << ": " << value << endl;
}
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
// cout << sizoef(args...) << endl;无法计算可变参数的大小
cout << typeid(value).name() << ": " << value << endl;
ShowList(args...); // 他会自动解析出一个类型给 value
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
1.3 逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),
(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。
由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template<class T>
void printarg(T value)
{
cout << typeid(value).name() << ": " << value << endl;
}
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { (printarg(args), 0)... };
// 编译器需要计算数组的大小,后面调用函数,每次传递一个参数。由逗号表达式,最后数组值都是0
// 三个点表示安装前面括号里面的方式依次展开
for (int i = 0; i < 3; i ++ )
{
cout << arr[i] << " ";
}
cout << endl;
}
int main()
{
//ShowList(1);
//ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
1.4 STL容器相关接口函数
#include <list>
#include <map>
int main()
{
list<map<string, int> > li;
li.push_front({{ "sort", 2 }}); // 传统插入
li.emplace_back("sort", 2); // 可变模板参数插入
}
二、lambda表达式
在C++98/03版本中,想要在函数内调用同一个参数传入的不同函数,现有的方法有
- 函数指针,比较麻烦
- 仿函数,实现一个特别简单的函数也需要写一个类,比如sort每换一种比较方式都要写一个对应的仿函数类。
C++11提供了一个匿名函数,可以很方便的在局部区域写出一个函数传入,就是lambda表达式。
2.1 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。所以一个最简的lambda表达式[]{}
, 该lambda函数不能做任何事情。
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this)
它们也可以混合使用int x = 10, y = 20; [=, &y](){ return x + y; };
, int x = 10, y = 20; [&, y](){ return x + y; };
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。该变量就可以像函数一样使用。
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。 [&, a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 - 无法捕获块作用域以外的变量,比如main函数里面无法捕捉全局变量。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。底层仍是仿函数,就像范围for的底层是迭代器一样。主要是编译器的优化。