仿函数的最大缺点是:
- 如果命名不合规范并且没有给出注释,直接阅读难以理解(排序的调用过程中,不查看仿函数细节,则很难直接得知是升序排序还是降序排序)
- 每次实现仿函数都需自己构建一个类
因此C++
从其他语言借鉴了lambda
表达式语法,让我们可以无需提前创建类,提高代码可读性。
1.lambda表达式的语法
lambda
表达式的格式为:[capture-list](parameters)mutable->return-type{statement}
capture-list
:捕捉列表,编译器识别[]
判定是否为lambda
函数,因此[]
不可省略。而捕捉列表本身可以根据父域(对于lambda
函数内的变量来说的父域)的上下文,获取变量供lambde
函数使用parameters
:参数列表,和函数的参数列表基本一致,可以为空mutable
:这是一个关键字,意思为“可变的/易变的”,默认情况下,lambda
函数总是类似const
函数的行为(除非使用引用捕捉,就不会有const
行为),而使用该关键字可以取消const
行为,并且参数列表此时不能忽略return-type
:返回值类型,和普通函数基本类似,没有返回值时可以省略,若返回值明确也会自动推导,也可以省略statement
:函数体,内部可以使用参数列表中的参数和捕获的变量列表
在lambda
函数定义中,也具有函数签名的特征,并且参数列表和返回值类型是可选部分,而捕捉列表和函数体可以为空。
因此最简单的lambda
函数为[]{}
,这个lambda
函数不能做任何事情。
2.lambda表达式的运用
让我们尝试使用lambda
表达式来书写一个Add()
。
#include <iostream>
using namespace std;
int main()
{auto add = [](int x, int y)->int { return x + y; };//整体代表一个 lambda 对象cout << [](int x, int y)->int { return x + y; }(1, 2) << endl;cout << add(1, 2) << endl;return 0;
}
而使用lambda
表达式就可以替代仿函数在sort()
中的使用:
#include <iostream>
#include "Date.hpp" //以前写的 Date 类
using namespace std;
int main()
{vector<Date> v = { { 2013, 7, 9 }, { 2022, 6, 5 }, { 2023, 4, 17 } };auto less = [](const Date& d1, const Date& d2)->bool { return d1 < d2; };sort(v.begin(), v.end(), less);return 0;
}
那捕捉列表怎么用呢?假设我们需要一个交换函数,使用lambda
有两种方式书写。
#include <iostream>
using namespace std;
int main()
{//1.设置用于交换的数int x = 10, y = 5;cout << "x=" << x << " " << "y=" << y << '\n';//2.用于交换的 lambda 表达式(指针参数版)auto swap1 = [](int* px, int* py){int tmp = *px;*px = *py;*py = tmp;};swap1(&x, &y);cout << "x=" << x << " " << "y=" << y << '\n';//3.用于交换的 lambda 表达式(引用参数版)auto swap2 = [](int& rx, int& ry){int tmp = rx;rx = ry;ry = tmp;};swap2(x, y);cout << "x=" << x << " " << "y=" << y << '\n';//4.用于交换的 lambda 表达式(列表捕捉版)auto swap3 = [&x, &y](){int temp = x;x = y;y = temp;};swap3();cout << "x=" << x << " " << "y=" << y << '\n';return 0;
}
而捕捉列表可以有多种语法形式来捕捉:
//特定
auto func1 = [x, y](){};//对应变量传值捕捉
auto func2 = [&x, &y](){};//对应变量引用捕捉(需要注意不要误认为是取地址)
auto func3 = [&x, y](){};//对应变量引用+对应变量传值混合捕捉//泛指
auto func4 = [&](){};//所有变量均可引用捕捉(包括 this)
auto func5 = [=](){};//所有变量均可传值捕捉(包括 this)
auto func6 = [&, x](){};//除了 x 参数是传值捕捉,其他都是引用捕捉
auto func7 = [this](){};//捕获当前的 this 指针
3.lambda表达式的误解
-
lambda
表达式的捕获列表只能捕获父域的变量,如果在父域内没有对应的变量,则会编译报错。注意,这个父域是对于lambda
内部的来说的,包含lambda
表达式所处的表达式#include <iostream> using namespace std; int main() {//交换int x = 10, y = 5;cout << "x=" << x << " " << "y=" << y << '\n';do {do {auto swap = [&x, &y](){int temp = x;x = y;y = temp;};swap();} while (0);} while (0);cout << "x=" << x << " " << "y=" << y << '\n';return 0; }
-
lambda
表达式产生的lambda
对象不允许相互赋值,这涉及到底层实现,不同的或者说哪怕相同的lambda
表达式结构生成的对象的类型均不一样,并且在不同编译器上运行都不一样(有些编译器的实时可能会看不出来)#include <iostream> using namespace std; int main() {//交换int x = 10, y = 5;auto swap1 = [&x, &y](){int temp = x;x = y;y = temp;};auto swap2 = [&x, &y](){int temp = x;x = y;y = temp;};cout << typeid(swap1).name() << '\n';cout << typeid(swap2).name() << '\n';//swap1 = swap2; //报错return 0; }
-
lambda
对象虽然不允许赋值,但是允许拷贝构造出一个新副本#include <iostream> using namespace std; int main() {//交换int x = 10, y = 5;auto swap = [&x, &y](){int temp = x;x = y;y = temp;};auto swapCopy(swap);cout << typeid(swap).name() << '\n';cout << typeid(swapCopy).name() << '\n';return 0; }
-
可以将
lambda
表达式赋值给和lambda
返回值相同类型的函数指针(但是带有捕捉列表的lambda
表达式不允许这么做)#include <iostream> using namespace std; void(*p)(int, int); int main() {//交换int x = 10, y = 5;auto swap = [](int x, int y){cout << "x = " << x << '\n';cout << "y = " << y << '\n';};p = swap;(*p)(x, y);return 0; }
#include <iostream> using namespace std;int main() {//交换int x = 10, y = 5;//lambda表达式没有捕获列表,可以直接赋值给函数指针auto func1 = []() { cout << "Hello, World!" << endl; };void(*ptr1)() = func1;ptr1(); //输出:Hello, World!//lambda表达式带有捕获列表,无法直接赋值给函数指针//int(*ptr2)() = [x, &y]() { return x + y; }; //编译错误return 0; }
-
如果是引用捕捉,无需使用关键字
mutable
也可以在lambda
表达式内修改。也就是说,捕捉变量实际上是一个变量拷贝了父域的变量,而引用则直接使用该变量(不过,如果一个是引用捕捉一个传值引用,就还是需要加上mutable
)#include <iostream> using namespace std; int main() {//交换int x = 10, y = 5;//auto func = [&x, y]()// {// x = 0;// y = 0;//出错,不允许改变// };auto func = [&x, y]()mutable //必须使用 mutable{x = 0;y = 0;//只是改变了局部变量};func();cout << "x = " << x << endl; //输出 x = 0cout << "y = " << y << endl; //输出 y = 5return 0; }
-
this
的传值可以让lambda
表达式成为类似成员函数的存在写入类中#include <iostream> using namespace std;class Data { public:void function(){auto func = [this](){cout << "x = " << x << ", y = " << y << '\n';x = 100;y = 500;cout << "x = " << x << ", y = " << y << '\n';};func();}private:int x = 10;int y = 50; }; int main() {Data d;d.function();return 0; }
4.lambda表达式的疑问
我们的确可以使用lambda
表达式解决了使用sort(v.begin(), v.end(), less)
的问题,但是我们还有一种情况会使用仿函数,就是在使用map
的时候会写出map<string, int, 仿函数类类型>
,对最后的仿函数需要将string
转化为key
,但是仿函数的类型我们尚且可以知道,那么lambda
怎么办呢?
lambda
的类型比较难以获取,并且还是匿名的,尽管有些其他的办法获取类型,但是我们更加推荐使用包装类function{}
,这些我们下一篇介绍。