前言
之前对右值引用的理解,用使用场景做了详细说明,具体看博客:
C++ - 右值引用 和 移动拷贝-CSDN博客
在 有值引用 当中还有一个 完美转发,请看本篇博客。
完美转发
我们现在看这个例子:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
上述当中的 PerfectForward(a); // 左值 当中,a 是一个左值,但是 PerfectForward()这个函数的参数类型是 T&& t 是一个 右值引用,按照上一篇博客当中对右值引用的介绍,右值引用是不能引用左值的,应该会编译报错。
但是,实际上是没有编译报错,编译通过。
其实,原因是 T&& t ,当中的T 是模版参数,模版当中的 T&& 已经不是右值引用了,这里的 T&& 叫做万能引用。
所谓万能引用,就是这个参数,就可以接收左值,有可以接收右值。因为此处是一个模版类型,可以传任意类型的变量进来,不像上篇当中写的一样,对于 左值引用 和 右值引用 是显示写出来的类型,是写死的类型。比如 : int&& ,double&& 都是右值引用。
也就是说,对于万能引用:
- 传入的模版实参是 左值,他就是左值引用(引用折叠)。 而所谓引用折叠就是,如果传入的实参是 左值的话,那么 T&& 就会 折叠为 T&。
- 传入的模版实参是 右值,它就是右值引用。也就是 T&&。
也就是说 : PerfectForward(10); 和 PerfectForward(a); 这两个函数不是同一个函数,他们是同一个模版实例化出来的两个函数,前者是 右值引用版本 ,后者是 左值引用版本。
了解 万能引用 之后,我们来看上述例子的输出:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
输出:
左值引用
左值引用
左值引用
const 左值引用
const 左值引用
我们发现,输出的结果有点怪,前三个 传入的参数不管是 左值 还是 右值,都是调用的左值的func()函数。
结果验证,发现,并不是全部都给 引用折叠了,我们直接把 PerfectForward ()函数的参数控制为 右值引用,发现,还是调用的左值引用版本的 func()函数:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
void PerfectForward(int&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(std::move(a)); // 右值
return 0;
}
输出:
左值引用
左值引用
我们先来看这个小例子:
int a = 1;
int& r = a;
int&& rr = move(a);
需要注意的是,上述 的 r 和 rr 都是左值,move(a)这个表达式是 右值,右值引用的 功能是右值引用,但是属性不是右值,而是左值,因为 右值引用支持修改。而且理论上右值引用必须支持修改,之前的移动拷贝,就是要修改 有值引用当中的数据,和 另一个对象当中的数据进行交换。
比如在 string 当中使用的移动拷贝:
我们使用 swap ()函数来进行交换两个string对象当中的数据,但是 swap()函数的参数是 string& 是一个 非const 的左值引用,是不能接受右值的,如果 string()拷贝构造函数当中的 s 有值引用的属性是左值的话, swap()怎么可能接受呢?
所以,看懂上述这个小例子,你应该就清楚,为什么上述 PerfectForward()函数不管传入左值还是有值,调用的都是 左值版本的func()了,因为右值引用的 功能是右值引用,但是属性不是右值,而是左值
完美转发的使用
那么,如上述例子,我们在模版当中,模版类型 的&& ,是万能引用,但是不管是 左值引用还是 右值引用,他们的属性都是左值,也就是说,如果在模版函数,或者说 模版类当中的成员函数当中,使用模版类型的形参 调用某个函数的话,只能是左值引用参数类型的函数。
但是,右值引用的优化,在很多地方都会用到,在模版函数,类模版的成员函数当中,也是非常多用的。所以,这时候就要使用完美转发了。
完美转发的使用方式 跟 move ()类似,使用 forward<模版参数>(模版类型)的方式来使用,如上述例子,应该这样修改:
template<typename T>
void PerfectForward(T&& t)
{
// 完美转发
Fun(forward<T>(t));
}
也就是说,上述的 t 这个变量,如果是 左值引用,就保持左值属性;如果是右值引用,就保持右值属性。
完整代码:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
输出:
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
我们发现,结果就符合我们的预期了。
在类模版当中比如写了一个函数的 左值引用版本 和 右值引用版本,但是又想只用一个的话,不能直接把 某一个删掉:
template<class T>
struct ListNode
{
T _val;
ListNode* _left;
ListNode* _right;
listNode(const T& x)
:_val(x)
,_left(nullptr)
,_right(nullptr)
{}
listNode(T&& x)
:_val(forward(x))
,_left(nullptr)
,_right(nullptr)
{}
}
比如上述,如果把左值引用的函数删除掉的话,那么就会编译报错,左值版本的 insert()和 push_back()当中使用的左值版本的 结点构造函数找不到了。
因为使用 完美转发有一个前提,就是 使用 的 模版参数是推导出来的,而不是示例化出来的。
函数模版当中的模版参数就是 推导出来的,而且 类模板的模版参数是我们在外部显示传入,也就是显示示例化的。
如,我们之前的例子,上述的 T 模版参数,是通过 函数的形参 t 推导出来的。
而,上述的list结点结构体当中的构造函数当中使用的模版参数是 类模板的模版参数,不是推导的,是我们在定义 list<T> 的时候传入的参数,已经定好了。
如果实在想要像上述一样,只写一个的话,就要在类模板当中加一个这个函数的函数模版:
完美转发的意义
我们上述说明了 右值引用的两种使用场景:
首先是 移动语义,也就是移动构造和 移动拷贝,在一定语义当中,因为需要对某一些空间的替换,移动,所以,右值引用的对象要可以修改,所以此时右值引用的属性是左值的,我们可以取地址对其中的 成员进行修改,所以这也是 右值引用的属性是 左值的一大原因。
但是在上述我们说的例子当中,在模版函数和 类模版的成员函数当中是用 模版类型的形参,调用某一函数的时候,如果不使用移动语义就只能是 左值引用版本的函数,这肯定不是我们期望的。
右值引用带来了这么大的性能提升,我们为什么不用呢?
那么,此时我们期望右值引用的属性是左值,但是不能直接替换,因为在上述的移动拷贝当中,要使用 左值的属性。
所以,这时候就有了完美转发的出现了。
如果像是移动拷贝这种 右值引用 一定要是 左值属性的,那么就直接使用右值引用即可;如果是想要继承 原本 左/右值引用的 对象的 属性(是左值引用属性就是左值,是右值引用属性就是右值),那么就使用完美转发的继承 左/右值引用 原本的属性即可。
而,移动构造,移动拷贝,并不是延长了某一个变量的生命周期,而是把 上一个变量的空间,转移到另一个 变量当中去了,你可以理解为 这个空间的生命周期延长了,但是上一个变量的声明周期没有延长,但是单独的空间是没有声明周期这个概念的,这个概念是在变量当中的。
在C++11 之后,每一个容器当中的 insert()和 push_back()函数都进行了 对 右值引用版本的实现,升级。那么在 push_back()函数当中我们一般是 复用 insert()函数就够用了的,但是如果在 右值引用版本的 push_back()复用 insert()函数的话,我们肯定是期望复用 右值引用版本的 insert()函数,那么我们不能直接使用 push_back()传入的 右值引用形参来调用 insert()函数了,因为 右值引用默认是 左值属性,所以这时候我们就要使用 forward 完美转发来强行把 这里的 右值引用函数的属性转化为 左值的属性。
而且,在一个类当中,这个右值引用可能会多次传,比如上述 从 push_back()函数传入到 insert()当中,可能要经历多个函数的传递,为了保证传递之后,还是右值的属性,在每一次传的时候都要进行 完美转发:
传的每一次都需要 完美转发才能继承上一个 属性。
lambda表达式
lambda 表达式的出身,和仿函数的比较
lambda表达式,可以说是用来替代 函数指针的,还可以替代一些仿函数的功能。
如果我们想要对 某一种比较方法进行替换的话,可以使用仿函数的方式来实现。
像是在 sort()函数的当中就使用仿函数来实现,目标数据排升序还是降序:
#include <algorithm>
#include <functional>
int main()
{
int array[] = { 4,1,8,5,3,7,0,9,2,6 };
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如上述所示,在库当中有两个仿函数,一个是less,一个是 greater;因为仿函数是用一个类当中的 operator()()运算符重载,来解决,普通operator()()同参数不能重载的问题。
像上述一样,我们就可以传入 less 或者 是 greater 类的 匿名对象来控制 sort()函数是排升序还是排降序了。
而,仿函数除了实现 比较顺序的选择,还可以自定义比较规则,比如在一个类当中,有 名字,价格 ,评价分数。那么我们可以先三个仿函数,分别可以按照 名字的 string类进行比较,还可以按照 价格的大小,或者评价分数的大小来进行比较。
比如我们自己来实现下述类的排序:
#include <functional>
#include <algorithm>
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
}
像上述就是使用仿函数的方式,实现,goods 类当中三个成员的三种比较方式。
上述只是仿函数最基本的用法,在哈希表 的 多种类型的 取模;unordered_map 和 unordered_set 两个容器在 底层哈希表当中吗,去 key 值;等等 都是可以用仿函数去实现的。
仿函数最喜欢的适用场景就是 在模版参数当中进行调用,当前模版实例化出的对象,需要什么类型的仿函数,那么可以在外部 显示传入 这个 仿函数的类型。无论是 程序员在编写代码,或者是 用户在使用 某一个 容器等等场景,仿函数都给我们带来了很大的 方便之处。
但是,仿函数固然好用,但是同样会引来一些问题:
- 因为 我们调用方式是,创建一个 仿函数的对象,然后在调用这个对象当中的 operator()()函数,那么调用方式就是和调用普通函数是一样的,是 类名()的方式调用。那么就会出现问题,比如上述按照 不同的成员进行比较,我们取的名字就是 类似 ComparePriceLess 这样的方式,但是如果有人取名字不按照 规范的形式去 定义,取一些 Compare1 , Compare2 ,Compare3 ········ 这些名字,那么只会苦了读代码的人。
- 而且 ,上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。
- 对于一些简单的,有些重复的算法支持,有点过于夸张,从上述也可以看出,对于 价格 和 评价分数这种算法一样的例子,它需要实现两个函数(operator()())函数去搞定
lambda表达式 语法
看语法之前,我们先来看一个例子:
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate; });
}
这个例子就是用 lambda 表达式,实现效果和 仿函数实现效果是一样的。
我们发现 lambda表达式 其实就是 匿名函数对象。
lambda 语法:
[capture-list] (parameters) mutable -> return-type { statement}
lambda表达式各部分说明:
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
如:
int x = 1, y = 2;
// 相当于函数的定义
auto add = [](int x, int y)->int {return x + y; };
// 相当于函数的调用
cout << add(x, y) << endl;
return 0;
而且,lambda 当中,如果明确知道了返回值类型,那么,返回值就可以不写:
int x = 1, y = 2;
// 相当于函数的定义
auto add = [](int x, int y) {return x + y; };
// 相当于函数的调用
cout << add(x, y) << endl;
return 0;
而且,在日常写 lambda表达式的时候,都是不写返回值的,因为 很多时候 lambda的返回值类型都是确定的。返回一个对象都是可以自动推导类型的,如上述的 x + y 一样。
当然,只是返回值类型可以省略,参数列表和 其中的函数定义是不能不能省略的。
在函数定义当中,不止可以写一句 return x+y ; 同样可以向普通函数一样写多个语句:
auto swap = [](int x, int y)->int {
int tmp = x;
x = y;
y = tmp;
};
只是需要注意的是,在最后要多一个分号,因为这本质上就是一个 语句,编辑器需要分号来识别:
lambda表达式当中调用其他函数
如果是 lambda 调用全局的函数,那么可以直接调用,没有问题:
void func()
{
cout << "func()" << endl;
}
int main()
{
auto swap = [](int x, int y)->int {
int tmp = x;
x = y;
y = tmp;
func();
};
return 0;
}
但是,如果是
lambda表达式所在的局部当中的函数就不能直接调用,会编译报错:
int main()
{
auto func = []()->void {cout << "func()" << endl; };
auto swap = [](int x, int y)->int {
int tmp = x;
x = y;
y = tmp;
func();
};
return 0;
}
报错:
lambda表达式当中的捕捉列表
如果,你想在 lambda表达式当中使用某一个变量的值,但是又不想把这个变量传进去,那么就可以使用 lambda 表达式的 捕捉列表。
在lambda表达式最开始的 "[]" 就是捕捉列表,编译器是根据开头的 "[]" 来判断接下来的代码是否是lambda 函数的。
而捕捉列表是 捕捉上下文代码当中的变量供 lambda使用。
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
如下面这个例子:
int a = 0, b = 2;
double rate = 2.555;
auto add1 = [](int x, int y)->int {return x + y; };
auto add2 = [](int x, int y) {return x + y; };
auto add3 = [rate](int x, int y) {return (x + y) * rate; };
cout << add1(a, b) << endl;
cout << add2(a, b) << endl;
cout << add3(a, b) << endl;
输出:
2
2
5.11
捕捉列表捕捉变量的方式
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
下面是上述 5 种捕捉变量方式的一些例子:
例1:
在 lambda表达式实现的 swap()函数当中,我想交换两个变量,但是不想以参数的方式传入这个两个变量的话,就可以传入这两个变量的引用,通过引用来修改到其中的值:
int x = 1;
int y = 2;
auto swap = [x, y](){
int tmp = x;
x = y;
y = tmp;
};
swap();
上述编译报错:
error C3491: “x”: 无法在非可变 lambda 中修改通过复制捕获
error C3491: “y”: 无法在非可变 lambda 中修改通过复制捕获
因为,单独捕捉 x 和 y,只是传值,是和 函数当中传入参数是一样的,因为 lambda 表达式本质上还是 函数的方式在调用,也是要创建函数栈帧的,上述 lambda 当中捕捉的 x 和 y 就和 函数当中的 形参类似,相当于是把 swap 外部的x 拷贝给 swap 当中的 x。
而且 lambda 的捕捉列表传值是 const 的传值,只能读,不能进行修改,所说上述的代码会报错。
如果实在想像函数传值传参的方式一样,在内部修改 函数内部的 形参的话,可以加上 mutable 关键词修饰:
int x = 1;
int y = 2;
auto swap = [x, y]() mutable{
int tmp = x;
x = y;
y = tmp;
};
swap();
mutable让 捕捉到的x 和 y 可以被修改了,但是 这两个变量依旧是 外部的拷贝。在 swap()当中的 x 和 y 可以被修改了,但是不会影响到 swap()函数外部的 x 和 y,因为 是类似于 传值的方式传参,不会修改到外部变量。
但是上述的方式使用得很少,传值的方式捕捉一般只是想取到 变量的值,要想修改 外部的变量,我们一般使用的是 捕捉引用的方式捕捉变量:
int x = 1;
int y = 2;
auto swap = [&x, &y](){
int tmp = x;
x = y;
y = tmp;
};
swap();
注意,上述的捕捉列表当中的 &x 和 &y 不是取 x 和 y 的地址,而是表示这两个变量的引用。我们可以认为是C++11在这里的语法上的妥协。只是在 捕捉列表当中这样写 是 表示引用,其他地方还是 取地址的语法。
我们还可以用 [&] 的方式来 以 引用捕捉 的方式 捕捉父作用域当中所有的 变量(父作用域指包含lambda函数的语句块):
int main()
{
int a = 0;
int b = 1;
int c = 2;
int d = 3;
auto swap = [&]{
a = 10;
b = 10;
c = 10;
d = 10;
};
swap();
cout << a << " "<< b << " " << c << " " << d << " " << endl;
}
输出:
10 10 10 10
注意:我们上述虽然传入参数,但是我们连参数列表的 "()" 都没有写,因为 ,如果没有参数的话,我们甚至连 "()" 都不用写。
除了上述使用 "[&]" 方式,我们还可以混合着使用:
比如:
auto swap = [&, a]{};
这个语句的意思就是,引用捕捉的 方式 捕捉 全部的 变量,除了 a 变量之外。a变量以 传值捕捉的方式捕捉。
而且 引用捕捉的方式非常的灵活:
- 如果捕捉的是 普通变量的引用,捕捉到的就是普通引用;
- 如果是 const 变量,捕捉的就是 const 的引用,在函数内是不能修改的。
当然 "[=]" 这种也支持上述的混合写法。
语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
lambda表达式之间不能相互赋值,即使看起来类型相同,但是会报错:
auto func1 = [](int x, int y) {return x < y; };
auto func2 = [](int x, int y) {return x < y; };
func1 = func2;
编译报错:
error C2679: 二元“=”: 没有找到接受“main::<lambda_2413383518bedbdc42de776d37947602>”类型的右操作数的运算符(或没有可接受的转换)
我们可以打印一下上述的 两个 lambda 函数的类型:
cout << typeid(func1).name() << endl;
cout << typeid(func2).name() << endl;
输出:
class <lambda_10047583502d56c84f61e3fd5f21e4ff>
class <lambda_2413383518bedbdc42de776d37947602>
发现其实类型是不一样的 ,类型的名是有 lambda + uuid 生成的,这个 uuid 是某个大佬做的算法,可以生成 极小概率重复的字符串。
这里就保证了,名字是基本不会冲突的。lambda的底层其实就是用仿函数实现的。 如果两个类的类名相同了,就会编译报错了。
而 上述的 func1(),func2(),两个是各自仿函数类生成的对象。对于这两个对象,对于我们是匿名的,但是编译器是知道这两个变量的 类型的。所以才需要用 auto 自动推导类型。
lambda 的底层是仿函数,我们查看 反汇编来观察:
我们发现,它显示调用了 lambda_uuid 这个类名的构造函数,构造出了一个对象,然后调用了这个 对象的 operator()()函数。为了防止类名冲突(重复),使用了 lambda + uuid 的方式来给这个 lambda(底层仿函数)命名。
之前,我们说 ,在 lambda 当中是不能调用 本局部域当中的函数的,其实,要想调用 局部域当中的函数,需要用 捕捉列表 捕捉这个函数,才能在 lambda 当中调用这个函数。
auto add1 = [](int x, int y)->int {return x + y; };
auto swap1 = [add1](int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
cout << add1(x, y) << endl;
func();
};
lambda表达式和 仿函数
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
在 C++当中就连底层实现,lambda 都是用 仿函数实现的。
但是 lambda 相对于 仿函数的类名取名时候更加严谨,因为 仿函数的名字是由写这个仿函数的人决定的,可能不会复符合规范命名。
在 lambda 当中就舍弃了 人来进行命名,在我们看来,lanmda 就是一个 匿名函数对象;但是在编译器看来,他是由 lambda + uuid 的方式组成的基本不会冲突的 名字。
但是 lambda 的使用比 仿函数更加 复杂,刚开始学的人可能对 lambda 的语法有很多疑问,但是,当熟练掌握 lambda 之后,lambda 其实是一个非常好用的语法。