1 lambda表达式语法

lambda表达式是一个匿名函数,恰当使用lambda表达式可以让代码变得简洁,并且可以提高代码的可读性。
用单独函数,代码分散,阅读时要跳来跳去

#include <vector>
#include <algorithm>
#include <iostream>

// 为了筛选偶数,要单独定义一个函数
bool isEven(int x) {
    return x % 2 == 0;
}

int main() {
    std::vector<int> v = {1,2,3,4,5,6};
    // 找第一个偶数
    auto it = std::find_if(v.begin(), v.end(), isEven);
    if (it != v.end()) std::cout << *it << std::endl;
    return 0;
}

用 lambda 就地写逻辑,代码 “所见即所得”

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> v = {1,2,3,4,5,6};
    // 筛选偶数的逻辑直接写在调用处,一眼看懂意图
    auto it = std::find_if(v.begin(), v.end(), [](int x) {
        return x % 2 == 0;
    });
    if (it != v.end()) std::cout << *it << std::endl;
    return 0;
}

• lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。
• lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
• [capture-list] :捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉,具体细节1.2中我们再细讲。捕捉列表为空也不能省略。
• (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
• ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
• {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。

int main()
{
 // ⼀个简单的lambda表达式 
 auto add1 = [](int x, int y)->int {return x + y; };
 cout << add1(1, 2) << endl;
 // 1、捕捉为空也不能省略 
 // 2、参数为空可以省略 
 // 3、返回值可以省略,可以通过返回对象⾃动推导 
 // 4、函数题不能省略 
 auto func1 = [] 
 {
 cout << "hello bit" << endl;
 return 0;
 };
 func1();
 //交换两个数
 int a = 0, b = 1; 
 auto swap1 = [](int& x, int& y)
 {
 int tmp = x;
 x = y;
 y = tmp;
 };
 swap1(a, b);
 cout << a << ":" << b << endl;
 return 0;
}

1.2捕捉列表

一、捕获核心作用
lambda 默认不能直接用外层变量,必须通过捕获列表 [] 才能使用。
二、3 种捕获方式
1.显式捕获
[x, y]:x、y 值捕获
[&x, &y]:x、y 引用捕获
[x, &y]:混合显式

2.隐式捕获
[=]:所有用到的外层变量 自动值捕获
[&]:所有用到的外层变量 自动引用捕获

3.混合隐式 + 显式(最常用)
[=, &x]:其余值捕获,x 引用捕获
[&, x, y]:其余引用捕获,x、y 值捕获
规则:第一个必须是 = / &,且后面必须和它相反。

三、捕获范围限制
只能捕获 定义位置之前 的局部变量
全局变量、静态局部变量 不用捕获,直接用
全局 lambda:捕获列表必须为空 []

四、mutable 关键字
默认:值捕获的变量是 const,不能修改
加 mutable:可以修改拷贝出来的值,不影响原变量
规则:加了 mutable,参数列表 () 不能省略

int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量 
auto func1 = []()
{
 x++;
};
int main()
{
 // 只能⽤当前lambda局部域和捕捉的对象和全局对象 
 int a = 0, b = 1, c = 2, d = 3;
 auto func1 = [a, &b]
 {
 // 值捕捉的变量不能修改,引⽤捕捉的变量可以修改 
 //a++;
 b++;
 int ret = a + b;
 return ret;
 };
 cout << func1() << endl;
 // 隐式值捕捉 
 // ⽤了哪些变量就捕捉哪些变量 
 auto func2 = [=]
 {
 int ret = a + b + c;
 return ret;
 };
 cout << func2() << endl;
 // 隐式引⽤捕捉 
 // ⽤了哪些变量就捕捉哪些变量 
 auto func3 = [&]
 {
 a++;
 c++;
 d++;
 };
 func3();
 cout << a <<" "<< b <<" "<< c <<" "<< d <<endl;
 // 混合捕捉1 
 auto func4 = [&, a, b]
 {
 //a++;
 //b++;
 c++;
 d++;
 return a + b + c + d;
 };
 func4();
 cout << a << " " << b << " " << c << " " << d << endl;
 // 混合捕捉1 
 auto func5 = [=, &a, &b]
 {
 a++;
 b++;
 /*c++;
 d++;*/
 return a + b + c + d;
 };
 func5();
 cout << a << " " << b << " " << c << " " << d << endl;
 // 局部的静态和全局变量不能捕捉,也不需要捕捉 
 static int m = 0;
 auto func6 = []
 {
 int ret = x + m;
 return ret;
 };
 // 传值捕捉本质是⼀种拷⻉,并且被const修饰了 
 // mutable相当于去掉const属性,可以修改了 
 // 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉ 
 auto func7 = [=]()mutable
 {
 a++;
 b++;
 c++;
 d++;
 return a + b + c + d;
 };
 cout << func7() << endl;
 cout << a << " " << b << " " << c << " " << d << endl;
 return 0;
}

1.3lambda的原理

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

• 上⾯的原理,我们可以透过汇编层了解⼀下,下⾯第⼆段汇编层代码印证了上⾯的原理。

class Rate
{
public:
 Rate(double rate) 
 : _rate(rate)
 {}
 double operator()(double money, int year)
 {
 return money * _rate * year;
 }
private:
 double _rate;
};
int main()
{
 double rate = 0.49;
  // 函数对象 
 Rate r1(rate);
 r1(10000, 2);
 // lambda
 auto r2 = [rate](double money, int year) {
 return money * rate * year;
 };
 r2(10000, 2);

调试代码并转到反汇编,可以看到:

在创建函数对象rate r1时,会调用rate类的构造函数。
在使用函数对象rate r2时,会调用rate类的()运算符重载函数。
在这里插入图片描述

观察lambda表达式时,也能看到类似的代码:

借助auto将lambda表达式赋值给r2对象时,会调用<lambda_uuid>类的构造函数。
在使用r2对象时,会调用<lambda_uuid>类的()运算符重载函数。
在这里插入图片描述
总结
Lambda 表达式 = 编译器自动生成的匿名类 + 自动生成的 operator () 重载

1.生成一个匿名类(没有名字,编译器自己知道)
2.把捕获列表变成类的成员变量
3.重载 operator() 运算符
4.把 lambda 函数体放进 operator() 里

注意
Lambda 底层是编译器自动生成的匿名类(仿函数),每个表达式对应唯一独立类,与手写仿函数原理完全一致。
VS 编译器实现:
类名格式:<lambda_uuid>
uuid:通用唯一识别码,算法生成不重复字符串
作用:确保每个 lambda 底层类名绝对唯一
通用规则:
不同编译器类名格式不统一
核心要求:每个 lambda 必须对应独一无二的类,类名形式由编译器自行决定
开发者无需关心具体类名,仅需使用 lambda 本身

更多推荐