一、前言
lambda
表达式是一个很有趣的东西,其出现的目的,本质上也是来简化我们程序员代码的。
这句话的意义在于,即使没有、你不学lambda
表达式,你也能正常开发程序,只是有了lambda
表达式之后。你代码写起来会更加舒服。
lambda
表达式有时候也被称为lambda
函数,或者直接简称为lambda
,它的作用是快速定义、使用匿名函数对象。
匿名函数对象同样也是一个有趣的知识点,有趣的并不是匿名,而是函数对象。
举个例子:
#include<iostream>
using namespace std;
class FunObject {
public:
int operator()(int x,int y) {
return x + y;
}
};
这里声明了一个FunObject
的类,然后我在其中重载了()
运算符。
而此时有意思的地方就来了,当我用这个类去定义一个对象后,就可以通过()
的方式来调用这个函数:
int main() {
FunObject add;
cout<<add(10,20);
}
这就和调用普通函数一样!
但和普通函数不同的地方在于,它不仅能当函数使用,也能当类使用,也就是说,它本身还可以存储一些属性的!
所以甚至进一步说,函数对象就是对普通函数的一次升级,可以完成更强大的功能。
甚至有些时候,函数对象的调用速度会比普通函数更快,这与编译器内联优化有关。
但它虽然好用,但你也看到了,它使用起来其实并不方便,因为还需要先去声明一个类才行。
而这就是lambda
的作用了,它就是用来简化这一过程的,即:生成匿名的函数对象,这样就不用去事先声明了。
二、基本使用
它的使用方法并不难,只要看懂了,我相信你一定会喜欢上使用它的。
#include<iostream>
using namespace std;
int main() {
int a = 100;
int b = 200;
//使用匿名函数对象,即lambda得到一个实例化的函数对象fun,auto为自动推断类型
auto fun = [a, b]() {
cout << a + b;
};
fun();
//可以简写为:
[a, b]() {
cout << a + b;
}(); //最后的(),代表直接调用
}
上面的代码中,[](){};
结构就是一个lambda
表达式的基本写法。
[]
:用来捕获变量交给后面的函数体使用()
:函数的参数{}
:函数体,存放代码。
如果将上面的代码转换为函数对象,那就是等价于下面这样:
#include<iostream>
using namespace std;
class TestFun {
int a;
int b;
public:
TestFun(int a,int b)
:a(a),b(b)
{}
void operator()() {
cout << a + b;
}
};
int main() {
int a = 100;
int b = 200;
TestFun fun(a, b);
fun();
}
也就是说,这个[]
捕获组,实际上就是自动将其转换为函数对象的成员变量,通过构造函数的初始化列表进行赋值的。
而后面的(){}
,本质上就是重载运算符()
的函数而已。
但通过写成lambda
表达式的形式,你会发现,它真的能让我们少写超级多的代码!
三、捕获组
虽然上面说了,这个捕获组本质上就可以看作是函数对象的构造函数,但还是有一些需要注意的点。
比如像前面那样,直接填入函数体需要的变量:
[a, b]() {};
这种是通过复制实现的,对于一些结构体、字符串来说,我们可能更希望使用引用的方式:
[&a, &b]() { };
这可以避免大量拷贝。
如果觉得一个一个的填太过麻烦,也可以直接像下面这样写:
[=]() {};
直接使用一个=
符号,它默认会按复制的方式捕获周围所有可用的变量。
如果你想要让某个变量按引用,其它都复制,那就可以这样写:
[=,&a](){}
这里的意思就是,除了a
变量使用引用的方式,其它都使用复制的方式传入。
同样也可以反过来:
[&](){}; //按引用的方式捕获所有变量
[&,a](){}; //除了a用复制,其它都是引用
如果lambda
表达式写在一个类的成员函数中,那你可能还想要捕获this
指针来使用这个类的成员变量:
[this](){};
当然,你也可以直接使用=
或者&
,它们同样也会将this
包含进来。
四、mutable与constexpr
默认情况下,lambda表达式捕获的变量值是不可变的:
如果你想要修改它的值,一种方式是使用引用捕获,但这样做会修改外部变量的值:
如果你仅仅只是想要在lambda内部修改使用、不影响原值,那么就可以使用mutable关键字:
此外,C++20引入了constexpr关键字,结合lambda表达式可以实现编译期间求值的功能,提高程序性能:
int main() {
int a = 10;
constexpr auto lambda = [](int x){
return x * x;
};
static_assert(lambda(10)==100);
}
而这里使用到的static_assert
是静态断言,如果在编译期间不满足填入的表达式就会直接报错,从这里我们就可以确定它是编译期间求值。
比如,如果我们填其它值,我们甚至都无法进行编译:
五、常见用途
正因为它使用起来非常的方便,我们大部分都是将其填入一些需要接收回调函数的函数参数中。
比如标准库中有一个sort
函数,它可以用来排序,可如果想要排序自定义数据类型,那么就可以使用它的第三个参数传入一个回调函数,而这个回调函数最简单的方式就是使用lambda
表达式:
#include<iostream>
#include<algorithm>
using namespace std;
struct Stu
{
string name;
int age;
};
int main() {
Stu s[5]{
{"小明",10},
{"小红",50},
{"小蓝",20},
{"小青",90},
{"小风",30},
};
sort(s, s + 5, [](Stu &a,Stu &b) {
return a.age < b.age; //前一个小于后一个,即从小到大排序
});
for (int i = 0; i < 5; i++) {
cout << s[i].name << ":" << s[i].age << endl;
}
}
这个sort
函数的前两个参数是要排序的范围,而第三个参数就是我们的lambda
表达式,sort
函数会将要比较的两个元素通过参数传入,也就是这里的Stu
类型,然后就可以自定义排序规则了。