文章目录
一. 为什么要有 lambda 表达式?
在 C++98 中,如果想要对一个数据集合中的元素进行排序,可以使用 std::sort 方法。如果待排序元素为自定义类型,此时需要用户自定义排序时的比较规则:
后面人们开始觉得上面的写法太复杂了:
- 如果每次比较的逻辑不一样,就要去分别实现多个仿函数
- 仿函数命名不规范的话(比如:Cpmpare1、Compare2 这样的),会给编程者带来了极大的不便。因此,在 C++11 语法中引入了 lambda 表达式。
二. 什么是 lambda 表达式?
1. 概述
下面是 lambda 表达式各个部分的说明:
- [capture-list] : 捕捉列表。该列表总是出现在 lambda 表达式的开始位置,它是 lambda 表达式的标识。编译器根据 [ ] 来判断接下来的代码是否为 lambda 表达式,捕捉列表能够捕捉上文作用区中的变量供 lambda 表达式使用。
- (parameters):参数列表。与普通函数的参数列表一致(可以指定参数是传值还是传引用,或者是否是 const),如果不需要参数传递,则可以连同括号一起省略。
- mutable:默认情况下,lambda 表达式的捕捉列表中,传值捕捉到的值都是 const 属性的,使用 mutable 可以取消这些传值捕捉对象的 const 量性。注意使用该修饰符时,参数列表不可省略。
- ->returntype:返回值类型。用追踪返回类型形式声明表达式的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型自动进行推导。
- {statement}:函数体。在该函数体内,除了可以使用参数列表中提供的形参外,还可以使用捕捉列表中,所有捕获到的变量。
注意: 在 lambda 表达式中,参数列表和返回值类型都可以省略不写,而捕捉列表和函数体可以为空,但不能省略不写。因此 C++11 中最简单的 lambda 表达式为:[]{}; 该 lambda 表达式不能做任何事情。
如果我们将 lambda 表达式写全,那便是如下的样子:
接下来,我们分别来看看,组成 lambda 表达式的这几个部分
参数列表 — ( )
示例:使用普通函数和 lambda 表达式,各自实现一个两数相加的功能:
可以看到,两者在使用上是没有区别的
有时,我们的 lambda 表达式不需要传入参数,此时连 ( ) 都可以省略掉:
捕捉列表 — [ ]
lambda 表达式和函数有一个区别是:函数先是在全局域中定义,然后到局部中去使用;而 lambda 表达式,它的定义和使用都在局部中,这个时候 lambda 表达式旁定然会存在着许多局部变量
int main()
{
//函数体里有大量局部变量
int a = 10;
char c = 20;
//lambda 表达式就定义在这些局部变量周围
auto lambda_func = []() {};
}
为了方便获得他们,lambda 表达式里便引入了一个新的参数列表——捕捉列表,它专门用来捕捉所属局部域中的其它参数,比如:
int main()
{
int a = 10;
int b = 20;
// 定义在 lambda 表达式之前的局部变量均可以捕捉到
auto lambda_func = [a, b] {cout << a << ' ' << b << endl; };
lambda_func();
int c = 30; // 在 lambda 表达式之后定义,所以不能被捕捉到
return 0;
}
------运行结果------
10 20
只需要在捕捉列表中输入在这个局部域中变量的名称,这个变量就能被捕捉了,可以在函数体中随意使用该变量,捕捉列表的本质其实也是传参,不要把它想的太神秘了。
但是,捕捉来的变量只是一个传值拷贝的局部变量,如果想要引用的方式捕捉一个变量,需要在待捕捉变量的前面加上取地址符号&:
// 引用捕捉 i,并修改它的值
auto lambda_func = [&i]() {i += 10;};
总结一下:捕捉列表描述了上文中那些数据可以被 lambda 使用,以及捕捉的方式是传值捕捉还是传引用捕捉。
- [var]:表示以传值的方式捕捉变量 var
- [&var]:表示以引用的方式捕捉变量 var
- [=]:表示以传值的方式捕获上文父作用域中的所有变量(包括成员函数中的 this)
- [&]:表示以引用的方式捕捉上文父作用域中的所有变量(包括成员函数中的 this)
- [this]:以值传递的方式传递 this 指针
代码示例
class Lambda
{
public:
void Func()
{
int i = 0;
//单个值传递
[i]{};
//所有值传递
[=]{};
//单个引用传递
[&i]{};
//所有引用传递
[&]{};
//传递 this指针
[this]{/*可以访问类中的_a*/ cout << _a << endl; };
}
private:
int _a;
};
同时,我们也可以传值和传引用一起搭配使用:
- 捕捉列表可由多个捕捉项组成,并以逗号分割
比如:[=, &a, &b] 代表以值传递方式捕捉除 a,b 以外的所有变量,以引用传递方式捕捉 a,b - 捕捉列表不能重复捕捉
[=, a, b] 重复捕捉了 a,b,此时编译器会报错
捕捉参数的性质 — mutable
看下面例子,我们以值传递方式捕捉变量 a 之后,想在函数体中修改 a 的值,但是运行后报错了:
这是因为,在 lambda 表达式的捕捉列表中,传值捕捉到的值默认都是 const 的,我们无法直接对其进行修改 ,想要修改的话,需要使用 mutable 关键字来改变它们的常属性。
关于 mutable 使用的几点注意事项:
- 使用该修饰符时,参数列表不可省略,即使没有参数也要写上 ()
- 捕捉列表中传值捕捉到的对象默认都是 const 的,而传引用捕捉的对象是非 const 的
- mutable 只针对传值捕捉的对象,对捕捉列表中的引用捕捉和参数列表中的形参没有任何影响。
->返回值
有人可能会好奇,为什么上面的例子都没有见到 -> 这个东西?
这是因为,C++ 抄 python 作业的时候,也借鉴它的弱类型语法的特点——当我们不指定返回值的类型时,编译器会自动推倒返回值的类型。所以,我们没有必要特意去定义返回值的类型,既然编译器会自动帮我们推导,那还要我们多笔干什么呢?
2. lambda 表达式的底层实现
每一个 lambda 表达式的类型名称都是不一样的,即使它们的定义是相同的:
所以,我们定义出来的 lambda 表达式是不能相互赋值的,因为其类型无法相互转换。但是,可以用一个已经定义好的 lambda 表达式去构造另一个新的 lambda 表达式:
下面我们调用一个 lambda 表达式,然后去汇编代码中看看它的底层到底是怎么调用的:
在底层,编译器对于 lambda 表达式的处理方式,完全就是按照函数对象的方式去处理的,即:如果定义了一个 lambda 表达式,编译器会自动生成一个类,然后在类中重载了 operator()。
三. lambda 表达式的使用
lambda 表达式实际上可以理解为无名函数,该函数无法直接调
用,如果想要调用,可借助 auto 将其赋值给一个变量,然后通过这个变量间接使用 lambda 表达式。
使用 lambda 表达式实现一个两数交换的功能:
int main()
{
// 标准写法
int a = 10, b = 20;
auto swap1 = [](int& x, int& y)->void
{
int tmp = x;
x = y;
y = tmp;
};
// 错误写法(传值捕捉,实际两个数并没有真正的交换)
auto swap2 = [a, b]()mutable
{
int tmp = a;
a = b;
b = tmp;
};
// 传引用捕捉写法
auto swap3 = [&] //[&a, &b]
{
int tmp = a;
a = b;
b = tmp;
};
return 0;
}
在处理一些小功能的时候,lambda 表达式可以极大提高程序的可读性:
int main()
{
vector<int> v = { 1,3,9,5,8,4,2 };
// 最初写法:传仿函数对象,然后内部在调用 operator()
std::sort(v.begin(), v.end(), greater<int>());
// lambda写法:简化程序,增强可读性
// 在底层也是调用 operator()
std::sort(v.begin(), v.end(), [](int num1, int num2)->bool
{return num1 < num2; });
}