文章目录
- [1. lambda表达式的语法](#1. lambda表达式的语法)
- [2. 举例](#2. 举例)
- [3. lambda的应用](#3. lambda的应用)
- [4. lambda捕捉列表](#4. lambda捕捉列表)
-
- [4.1 传值捕捉与传引用捕捉](#4.1 传值捕捉与传引用捕捉)
- [4.2 隐式捕捉与显式捕捉](#4.2 隐式捕捉与显式捕捉)
- [4.3 混合捕捉](#4.3 混合捕捉)
- [4.4 全局变量和静态变量的捕捉](#4.4 全局变量和静态变量的捕捉)
- [4.5 mutable](#4.5 mutable)
- [5. lambda的原理](#5. lambda的原理)
1. lambda表达式的语法
lambda 表达式本质是⼀个匿名函数对象 ,跟普通函数不同的是他可以定义在函数内部(而普通函数通常是定义在全局的)。
lambda 表达式在语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收lambda对象。
lambda表达式的格式:
[capture-list] (parameters)-> return type { function boby }
分别代表什么意思呢?
[capture-list]: 捕捉列表
该列表总是出现在 lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使用,捕捉列表可以传值或传引用捕捉,具体细节我们后面再细讲。捕捉列表为空也不能省略。
(parameters):参数列表
与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
->return type:返回值类型
用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
{function boby}:函数体
函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
与我们之前学过的普通函数相比:
相信只有捕捉列表我们是比较陌生的,其它几个东西我们都明白它是什么意思。
另外我们发现它是没有函数名的,这也是它为什么叫做匿名函数的原因。
2. 举例
那下面就带大家来简单写一个lambda表达式,练习一下。
比如:
这里赋值符号右边的就是一个lambda表达式,或者说一个匿名函数对象。
我们用一个auto定义的变量add来接受这个匿名函数,编译器会自动推导它的类型。
当然我们也可以调用这个匿名函数
那么最开始的[]就是捕捉列表,我们后面会详细讲解。
但是要注意:
- 捕捉列表为空也不能省略
就像上面的例子,没有捕捉任何东西,但是[]是不能省略的。- 返回值类型可以省略
编译器可以通过返回对象自动推导- 参数为空则参数列表可以省略
比如我们这个匿名函数只是完成一个字符串的打印,不需要参数
这里我们参数列表和返回值都省略了
注意这种情况:
如果写了返回值类型,但是参数列表为空时,至少要写()
- 函数体不能省略
再比如,写一个交换两个变量的匿名函数对象
3. lambda的应用
在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象
函数指针的定义和书写比较复杂麻烦,仿函数又要定义⼀个类,也相对比较麻烦。某些场景下使用 lambda 去定义可调用对象,既简单又方便。
比如,我们来看这样一个场景:
现在有一个描述某种商品的结构体。定义了一个结构体数组,现在我们要对这组商品进行排序
我们要想直接调用sort对这个vector进行排序肯定是不行的,因为里面存储的元素是自定义类型的Goods变量
当然我们可以针对Goods类对>或<操作符进行重载,实现我们想要的比较规则,比如按价格或评价升序降序去排。
比如,我们按价格比较升序
当然,我们也可以传函数指针或仿函数
比如这里也按价格升序,那这里大家仿函数的命名就建议最好有意义一些,如果你写成一个compare1(可读性很差)
那我只看代码,就不好判断出来你是按什么排序,升序还是降序,还要点开对应的仿函数看它的实现逻辑
但是,这种场景:
如果比较的逻辑比较简单,那这里的可调用对象我传一个lambda表达式过去就会非常简洁清晰。
sort的第三个模板参数,我们直接传一个lambda表达式就可以了
当然,上面只是简单的列举了一类lambda的使用场景, 在很多其他地方lambda用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。
4. lambda捕捉列表
那下面我们来重点学习一下lambda的捕捉列表
lambda 表达式中默认只能用lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
比如:
在这个lambda表达式里面,我们想使用变量a和b是不行的,因为变量a和b在lambda的外层作用域中。要想访问,我们就需要进行捕捉。
捕捉呢又具体分为:
传值捕捉、传引用捕捉、隐式捕捉、显式捕捉
4.1 传值捕捉与传引用捕捉
传值捕捉(直接在捕捉列表中写对应
变量名)的变量不能修改,传引用捕捉(捕捉列表中写&变量名)的变量可以修改,并且修改会影响外层被捕捉变量。
捕捉的多个变量用逗号分割。
如:[x,y, &z] 表示x和y值捕捉,z引用捕捉。
当然,全局对象即使不捕捉也可以直接用
比如,在上面的例子中
对变量a进行值捕捉,b进行引用捕捉
然后就可以成功访问了。
b是引用捕捉可以修改
a是值捕捉不可修改
4.2 隐式捕捉与显式捕捉
上面的例子中我们显式指定了我们要捕捉的变量,叫做显式捕捉。
除了显式捕捉,还有隐式捕捉
隐式捕捉同样分为值捕捉和引用捕捉
在捕捉列表写⼀个=表示隐式值捕捉,这样我们在lambda中用到哪些变量,就会自动值捕捉这些变量
当然值捕捉就不能修改它们。
在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了哪些变量,编译器就会自动引用捕捉那些变量,既然是引用捕捉那就可以修改
4.3 混合捕捉
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显式捕捉。
=, \&x\]表示其他变量隐式值捕捉,x引用捕捉; \[\&, x, y\]表示其他变量引用捕捉,x和y值捕捉。 当使用混合捕捉时,第⼀个元素必须是`&`或`=`,并且\&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
比如:
a,b引用捕捉,其余用到哪个变量就值捕捉哪个变量
但要注意,这样写的话,一定要把=写到前面
a,b值捕捉,其余引用捕捉
4.4 全局变量和静态变量的捕捉
lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量 和全局变量 ,静态局部变量和全局变量也不需要捕捉 , lambda 表达式中可以直接使用 。
这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。(当然lambda我们一般不会写到全局)
比如:
4.5 mutable
传值捕捉本质是一种拷贝,并且被const修饰(所以值捕捉的变量不能修改)
mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是其拷贝,不会影响原始被捕捉对象。
使用该修饰符后,参数列表不可省略(即使参数为空)。
mutable相当于去掉const属性,可以修改了,但是修改了不会影响外面被捕捉的值
5. lambda的原理
lambda 的原理和范围for很像
编译后从汇编指令层的角度看,压根就没有 lambda 和范围for这样的东西。
范围for底层是迭代器,而lambda底层是仿函数对象 ,也就说我们写了⼀个lambda 以后,编译器会生成⼀个对应的仿函数的类。
仿函数的类名是编译器按⼀定规则生成的,保证不同的 lambda 生成的类名不同,lambda表达式的参数/返
回类型/函数体就是对应仿函数operator()的参数/返回类型/函数体。
lambda 的捕捉列表本质是对应仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看用到哪些就传那些对象。
上面的原理,我们可以透过汇编层了解⼀下
现在这里有一个仿函数
然后再来写一个lambda
lambda和仿函数的参数列表和函数体一样,lambda捕捉列表的变量和仿函数的成员变量一样。
观察一些它们底层的汇编
我们可以看到,lambda底层也是去调用了operator()





























