1. 简单介绍及语法
Lambda表达式是C++11引入的一种便捷的匿名函数定义机制。
lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收 lambda 对象。
Lambda表达式的基本语法
Lambda表达式的基本语法格式如下:
[capture_list](parameters) -> return_type { function_body }
- capture_list:捕获列表(不可省略),该列表总是出现在 lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 lambda 函数用于指定Lambda表达式可以访问的外部变量,以及捕获的方式(按值或按引用)。
parameters
:参数列表(可以省略),定义传递给Lambda表达式的参数。如果不需要参数传递,则可以连同()⼀起省略-> return_type
:可选的尾部返回类型指定(可以省略)。如果省略,则由函数体中的返回语句推断返回值。function_body
:Lambda表达式的函数体(不可省略),包含执行的代码,函数体为空也不能省略。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
#include<iostream>
using namespace std;int main()
{// ⼀个简单的lambda表达式auto add = [](int x, int y)->int {return x + y; };cout << add(1, 2) << endl;// 1、捕捉为空也不能省略// 2、参数为空可以省略// 3、返回值可以省略,可以通过返回对象自动推导// 4、函数题不能省略auto hello = []{cout << "hello World" << endl;return 0;};hello();int a = 0, b = 1;auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};swap1(a, b);cout << a << ":" << b << endl;return 0;
}
2. 捕捉列表
在lambda表达式中不可以直接使用外部变量,因为其本质上是一个函数,要使用其所在域内的变量,需要用捕捉列表进行捕捉。
捕捉的方式有:显式传值捕捉,显式传引用捕捉,隐式传值捕捉,隐式传引用捕捉。
显式传值捕捉与传引用捕捉
第一种捕捉方式是在捕捉列表中显式的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。
直接将变量写到捕捉列表中时为传值捕捉,加上 "&" 则表示传引用捕捉:
[x,y,&z] 表示x和y值捕捉,z引用捕捉。
注意:传值捕捉的变量是被const修饰的,不可修改。
int main()
{// 只能用当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;auto func1 = [a, &b]{// 值捕捉的变量不能修改,引用捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};cout << func1() << endl;return 0;
}
隐式传值捕捉与传引用捕捉
"[=]" :捕捉列表中传入 "=" 表示自动传值捕捉在函数体内被用到的变量;
"[&]":捕捉列表中传入 "&" 表示自动传引用捕捉在函数体内被用到的变量。
int main()
{// 只能用当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;// 隐式值捕捉// 用了哪些变量就捕捉哪些变量auto func2 = [=]{int ret = a + b + c;return ret;};cout << func2() << endl;// 隐式引用捕捉// 用了哪些变量就捕捉哪些变量auto func3 = [&]{a++;c++;d++;};func3();cout << a << " " << b << " " << c << " " << d << endl;return 0;
}
混合使用显式捕捉和隐式捕捉
在捕捉列表中混合使用隐式捕捉和显式捕捉:
[=,&x] 表示其他变量隐式值捕捉,x引用捕捉;
[&, x, y] 表示其他变量引用捕捉,x和y值捕捉。
当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
int main()
{// 只能用当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;// 混合捕捉1auto func4 = [&, a, b]{//a++;//b++;c++;d++;return a + b + c + d;};func4();cout << a << " " << b << " " << c << " " << d << endl;// 混合捕捉2auto func5 = [=, &a, &b]{a++;b++;/*c++;d++;*/return a + b + c + d;};func5();cout << a << " " << b << " " << c << " " << d << endl;return 0;
}
不能捕捉静态局部变量和全局变量
lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。
这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
int x = 0;// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = [](){x++;};int main()
{// 只能用当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;// 局部的静态和全局变量不能捕捉,也不需要捕捉static int m = 0;auto func6 = []{int ret = x + m;return ret;};return 0;
}
mutable 修饰符
默认情况下, 传值捕捉的对象是被const修饰的,也就是说传值捕捉过来的对象不能修改。
mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。
使用该修饰符后,参数列表不可省略(即使参数为空)。
int main()
{// 只能用当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;// 传值捕捉本质是⼀种拷被,并且被const修饰了// mutable相当于去掉const属性,可以修改了// 但是修改了不会影响外面被捕捉的值,因为是⼀种拷贝auto func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};cout << func7() << endl;cout << a << " " << b << " " << c << " " << d << endl;return 0;
}
3. lambda表达式的应用
在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。
例如在下面的例子中,将lambda表达式直接作为可调用对象传给sort函数,不仅省去了仿函数的定义,而且将比较逻辑与sort函数绑定到一起,提高了可读性。
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 } };// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中// 不同项的比较,相对还是⽐较麻烦的,那么这里lambda就很好用了sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());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;});return 0;
}
4. lambda表达式的原理
编译器在编译时,会根据我们所写的lambda表达式生成一个仿函数的类,并返回该类的对象。
仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体。
lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
在下面这个例子中,r1和r2除了类名不同以外,是完全等价的。
class Rate
{
public:Rate(double rate): _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};int main()
{double rate = 0.49;// 函数对象Rate r1(rate);// lambdaauto r2 = [rate](double money, int year) {return money * rate * year;};r1(10000, 2);r2(10000, 2);return 0;
}
我们可以使用编译器的转到反汇编的功能来看二者的底层是否一样:
r1(10000, 2);
00007FF71CBD1A18 mov r8d,2
00007FF71CBD1A1E movsd xmm1,mmword ptr [__real@40c3880000000000 (07FF71CBDBDA8h)]
00007FF71CBD1A26 lea rcx,[r1]
00007FF71CBD1A2A call Rate::operator() (07FF71CBD1172h)
00007FF71CBD1A2F nop r2(10000, 2);
00007FF71CBD1A30 mov r8d,2
00007FF71CBD1A36 movsd xmm1,mmword ptr [__real@40c3880000000000 (07FF71CBDBDA8h)]
00007FF71CBD1A3E lea rcx,[r2]
00007FF71CBD1A42 call `main'::`2'::<lambda_1>::operator() (07FF71CBD1EA0h)
00007FF71CBD1A47 nop
可以看到,二者都调用了 "operator()" 。