一.Lambda

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。

1.1Lambda 表达式

Lambda 表达式是一种匿名函数对象,它可以在代码中直接定义并使用。其基本语法结构如下:

代码语言:javascript

AI代码解释

[capture](parameters)->return-type { body }
  • capture:捕获列表,用于捕获 Lambda 表达式外部的变量。捕获方式有值捕获([x],将变量 x 的值复制到 Lambda 中)、引用捕获([&x],通过引用捕获变量 x)等多种方式。
  • parameters:参数列表,与普通函数的参数列表类似,用于接收调用时传入的参数。
  • return-type:返回类型,可以省略,如果 Lambda 表达式只有一条语句且该语句有返回值,则返回类型由该语句的返回类型推导得出。
  • body:函数体,是 Lambda 表达式的核心部分,用于实现具体的功能逻辑。
1.2捕捉列表
捕获列表的基本概念

捕获列表位于 Lambda 表达式的开头,用方括号[]表示。它允许 Lambda 表达式捕获外部作用域中的变量,从而在 Lambda 表达式的函数体内使用这些变量。捕获列表的语法如下: [capture1, capture2, …] 捕获列表中的每个捕获项可以是一个变量名、一个变量名前加&符号(表示通过引用捕获),或者是一个变量名前加*符号(表示捕获变量的地址)。捕获项之间用逗号分隔。

捕获方式
(一)值捕获

值捕获是将外部变量的值复制到 Lambda 表达式中。这种方式下,Lambda 表达式内部的变量是外部变量的一个副本,对内部变量的修改不会影响外部变量。例如:

代码语言:javascript

AI代码解释

int x = 10;
auto lambda = [x]() {
    x = 20; // 修改的是 Lambda 内部的 x 副本
};
lambda();
std::cout << x << std::endl; // 输出 10,外部 x 的值未改变

值捕获适用于那些不需要修改外部变量,或者希望在 Lambda 表达式中独立操作变量的情况。

(二)引用捕获

引用捕获是通过引用的方式捕获外部变量。这种方式下,Lambda 表达式内部的变量是外部变量的引用,对内部变量的修改会直接影响外部变量。例如:

代码语言:javascript

AI代码解释

int x = 10;
auto lambda = [&x]() {
    x = 20; // 直接修改外部变量 x
};
lambda();
std::cout << x << std::endl; // 输出 20,外部 x 的值被修改

引用捕获适用于那些需要在 Lambda 表达式中修改外部变量的情况,但需要注意的是,如果外部变量的生命周期短于 Lambda 表达式的生命周期,可能会导致悬空引用,引发运行时错误。

(三)默认捕获

代码语言:javascript

AI代码解释

在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=,&x]表⽰其他变量隐式值捕捉,
x引⽤捕捉;[&,x,y]表⽰其他变量引⽤捕捉,x和y值捕捉。
当使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时
后⾯的捕捉变量必须是值捕捉,
同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。

除了显式指定捕获的变量外,Lambda 表达式还支持默认捕获。默认捕获有两种方式: [=]:表示默认按值捕获所有外部变量。 [&]:表示默认按引用捕获所有外部变量。 默认捕获可以简化捕获列表的书写,但需要谨慎使用,因为过度捕获可能会导致不必要的性能开销或潜在的错误。例如:

代码语言:javascript

AI代码解释

int x = 10;
int y = 20;
auto lambda = [=]() {
    x = 30; // 修改的是 Lambda 内部的 x 副本
    y = 40; // 修改的是 Lambda 内部的 y 副本
};
lambda();
std::cout << x << " " << y << std::endl; // 输出 10 20,外部 x 和 y 的值未改变

在默认捕获的情况下,如果需要对某个变量采用不同的捕获方式,可以通过显式捕获来覆盖默认捕获。例如:

代码语言:javascript

AI代码解释

int x = 10;
int y = 20;
auto lambda = [=, &y]() {
    x = 30; // 修改的是 Lambda 内部的 x 副本
    y = 40; // 直接修改外部变量 y
};
lambda();
std::cout << x << " " << y << std::endl; // 输出 10 40,外部 x 的值未改变,y 的值被修改

在这个例子中,x 按值捕获,y 按引用捕获。

(四)捕获其他特性
  1. lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

代码语言:javascript

AI代码解释

// 局部的静态和全局变量不能捕捉,也不需要捕捉 
 static int m = 0;
 auto func6 = []
 {
 int ret = x + m;
 return ret;
 };
  1. 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。

i

代码语言:javascript

AI代码解释

// 传值捕捉本质是⼀种拷⻉,并且被const修饰了 
 // mutable相当于去掉const属性,可以修改了 
 // 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉ 
 auto func7 = [=]()mutable

 {
 a++;
 b++;
 c++;
 d++;
 return a + b + c + d;
 };
 cout << func7() << endl;
(五)在类中捕获私有

在类中,若想要使用私有成员变量时,直接捕获私有成员是报错的;这时需要通过捕获this指针来使用私有成员变量。

1.3lambda的应⽤

• 在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对象,既简单⼜⽅便。

• lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等

代码语言:javascript

AI代码解释

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;
}
1.4lambda的原理

• lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的类。• 仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象。

更多推荐