目录
[1.1 lambda表达式语法](#1.1 lambda表达式语法)
[1.1.1 什么是lambda表达式](#1.1.1 什么是lambda表达式)
[1.1.2 lambda表达式的格式](#1.1.2 lambda表达式的格式)
[1.2 捕捉列表](#1.2 捕捉列表)
[1.2.1 捕捉列表的作用](#1.2.1 捕捉列表的作用)
[1.2.2 捕捉规则](#1.2.2 捕捉规则)
[1.2.3 捕捉方式](#1.2.3 捕捉方式)
[1.3 lambda的应用](#1.3 lambda的应用)
[1.4 lambda的原理](#1.4 lambda的原理)
[1.5 lambda与函数指针](#1.5 lambda与函数指针)
[1.5.1 普通函数与函数指针](#1.5.1 普通函数与函数指针)
[1.5.2 lambda不是函数指针?](#1.5.2 lambda不是函数指针?)
[1.5.3 为什么无捕获 lambda 可以赋值给函数指针?](#1.5.3 为什么无捕获 lambda 可以赋值给函数指针?)
[1.5.4 有捕获 lambda 为什么不行?](#1.5.4 有捕获 lambda 为什么不行?)
[1.5.5 总结:](#1.5.5 总结:)
1.1 lambda表达式语法
1.1.1 什么是lambda表达式
lambda 表达式本质上会生成一个匿名的闭包对象(Closure Object),与普通函数不同的是,它既可以像函数一样被调用,又能够捕获并保存其所在作用域中的变量。Lambda 表达式的类型是一个由编译器生成的、唯一的、匿名的闭包类型(Closure Type),由于该类型名无法直接书写,因此在实际开发中通常使用
auto、模板参数或std::function来接收和使用 lambda 对象。
关于闭包对象和闭包类型的具体实现原理,我们将在后续 lambda 原理章节中详细介绍。
1.1.2 lambda表达式的格式
[capture-list] (parameters)-> return type { function body }
capture-list:捕捉列表,该列表位于lambda函数的开始位置,捕捉列表为空也不能省略,因为编译器需要根据捕捉列表来判断接下来的代码是否为lambda函数,具体细节将在后续捕捉列表章节里详细讲解。
(parameters):参数列表,与普通函数的参数列表功能一样,用来接受传递的参数,如果不需要传递参数,则该参数列表可以省略不写。
-> return_type:返回值类型,与普通函数返回值功能一样,但是对于没有返回值的lambda函数,此部分可以省略不写,对于返回值类型明确的函数,此部分也可以省略不写,由编译器根据返回值的类型进行推导返回类型。
{ function body }:函数体,与普通函数的函数体一样,但在该函数体内,可以使用捕捉列表中捕获的变量,函数体为空也不能省略。
示例:
cpp
#include <iostream>
using namespace std;
int main()
{
// 一个完整的lambda表达式
auto add1 = [](int a, int b)->int
{
return a + b;
}; // 这个分号一定要加,它不是lambda语法所需要的,它是赋值这条语句所需要的
// 跟函数的使用方法一摸一样
cout << add1(3, 5) << endl;
// 1.捕捉列表为空不能省略
// 2.参数为空可以省略
// 3.返回值可以省略
// 4.函数体不可以省略
auto print = []
{
cout << "hello world" << endl;
};
print();
int a = 3, b = 5;
auto my_swap = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
my_swap(a, b);
cout << a << " " << b << endl;
return 0;
}
1.2 捕捉列表
1.2.1 捕捉列表的作用
在lambda表达式中变量的使用规则跟普通函数一样,如lambda函数体和参数中的变量,全局变量等,但lambda通过捕捉列表可以使用lambda对象所处域中的变量。
1.2.2 捕捉规则
普通捕获列表(如x, \&x)只能捕获具有名字的变量,通常是左值。对于临时对象或移动语义场景,需要使用 C++14 引入的初始化捕获(Init Capture),例如x = 10 或 p = std::move(ptr)。
cpp
// C++11
// 合法
int x = 10;
auto f = [x]()
{
return x;
};
// 不合法
auto f = [10]()
{
return 10;
};
// C++14 这里的捕捉不是普通捕捉,而是初始化捕捉,后续lambda底层原理会讲解原因
auto f = [x = 10]()
{
return x;
};
auto ptr = std::make_unique<int>(100);
auto f = [p = std::move(ptr)]()
{
return *p;
};
lambda 表达式如果在函数局部域中,它可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda 表达式中可以直接使用。这也意味着lambda 表达式如果定义在全局位置,捕捉列表必须为空。
cpp
int x = 0;
// 对于全局的lambda表达式,不需要捕捉任何变量
// 因为全局变量不用捕捉就可以使用,所以捕捉列表必须为空
auto func1 = []()
{
++x;
};
// 编译错误
// 无法在lambda表达式中捕捉带有静态存储持续时间的变量,
// 也就是无法捕捉静态区的变量
// auto func2 = [x]()
// {
// ++x;
// };
// [&x] 这样写也不对
//
默认情况下,lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
cpp
#include <iostream>
using namespace std;
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;
// 传值捕捉本质是一种拷贝,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外面被捕捉的值,因为是一种拷贝
auto func2 = [a, b, c, d]() mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
return 0;
}
1.2.3 捕捉方式
第一种捕捉方式:在捕捉列表中显示的传值 捕捉和传引用捕捉,捕捉的多个变量用逗号分割。
例如x, y, \&z 表示 x 和 y 是值捕捉,z 是引用捕捉。
第二种捕捉方式:在捕捉列表中隐式捕捉,我们在捕捉列表中写一个**"=" 表示隐式值捕捉** ,在捕捉列表中写一个 "&" 表示隐式引用捕捉 ,编译器会根据函数体内使用了哪些外部变量,就捕捉哪些。编译器不会捕捉全部的外部变量。
第三种捕捉方式:在捕捉列表中混合使用隐式捕捉和显示捕捉,=, \&x 表示其他变量隐式值捕捉,x 变量显示引用捕捉。\&, x, y表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
cpp
#include <iostream>
using namespace std;
int main()
{
// 传值捕捉和传引用捕捉
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
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]
{
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉2
auto func5 = [=, &a, &b]
{
a++;
b++;
return a + b + c + d;
};
func5();
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
1.3 lambda的应用
在学习lambda表达式之前,我们使用可调用对象只有函数指针和仿函数,函数指针的类型定义起来比较麻烦,仿函数需要定义一个类,也比较麻烦。使用lambda去定义可调用对象,既简单又方便。lambda在很多地方用起来也很好用,比如线程中定义线程的执行函数逻辑,智能指针中定义删除器等。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
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.4 lambda的原理
闭包类型 = 类
cpp
// 对于lambda表达式
auto f = [x](int y)
{
return x + y;
};
// 编译器在编译时会生成一个类
class __ClosureType
{
private:
int x;
public:
__ClosureType(int value)
: x(value)
{}
int operator()(int y) const
{
return x + y;
}
};
// 对于 __ClosureType 这个编译器生成类就是闭包类型
闭包对象 = 用闭包类型定义一个对象
cpp
__ClosureType f(x);
// f 就是闭包对象
为什么叫闭包?
因为它把「函数 + 运行环境」封装在一起了。
lambda的本质就是编译器会生成一个对应的仿函数的类。仿函数的类名是编译时按照一定的规则生成的,保证生成不同的lambda对象的类名,lambda参数、返回类型、函数体就是仿函数operator()的参数、返回类型、函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,用来走初始化列表初始化生成的仿函数类的成员变量,对于隐式捕捉,编译器首先检查函数体内使用了哪些外部变量,从而编译时生成对应的成员变量。对于值捕捉的变量默认情况下自带const属性,是因为编译器生成的operator被const修饰,无法修改成员变量。

函数调用
示例:通过汇编层面理解lambda表达式的本质
cpp
#include <iostream>
using namespace std;
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;
// lambda
auto r2 = [rate](double money, int year)
{
return money * rate * year;
};
// 函数对象
Rate r1(rate);
r1(10000, 2);
r2(10000, 2);
return 0;
}

1.5 lambda与函数指针
很多初学者会认为lambda既然能像函数一样调用,那么它应该就是一个函数指针,实际上并不是,lambda的本质是闭包对象,而函数指针只是一个保存函数地址的指针,两者完全不是同一个东西。
1.5.1 普通函数与函数指针
cpp
void func()
{
cout << "hello" << endl;
}
// 函数名可以退化成函数指针
void (*p)() = func;
p();
func();
// 此时 p() 和 func() 是等价的
// 这里的 p 保存的是函数入口地址
1.5.2 lambda不是函数指针?
cpp
auto f = []()
{
cout << "hello" << endl;
};
// 编译器会推导为一个类
class ClosureType
{
public:
void operator()() const
{
cout << "hello" << endl;
}
};
f(); 实际上是 f.operator()();
因此lambda是对象,不是函数指针
1.5.3 为什么无捕获 lambda 可以赋值给函数指针?
cpp
auto f = []()
{
cout << "hello" << endl;
};
// 合法
void (*p)() = f;
// 为什么呢?
// 因为无捕获 lambda 没有状态
// 编译器会额外生成一个静态函数
class ClosureType
{
public:
void operator()() const
{
cout << "hello" << endl;
}
static void __invoke()
{
cout << "hello" << endl;
}
};
// 然后void (*p)() = f; 等价于 void (*p)() = &ClosureType::__invoke;
// 能够隐式类型转换为函数指针
1.5.4 有捕获 lambda 为什么不行?
cpp
int x = 10;
auto f = [x]()
{
cout << x << endl;
};
// 编译器生成的
class ClosureType
{
private:
int x;
public:
ClosureType(int value)
: x(value)
{
}
void operator()() const
{
cout << x << endl;
}
};
// 这里的 x 已经称为对象的成员变量
// 调用时需要this->x 才能访问
// 因此 f(); == f.operator()(); 需要对象参与
// 而函数指针只有void (*p)() 它只知道函数地址,不知道对象地址
// 因此无法完成调用 void (*p)() = f; 就会编译失败
1.5.5 总结:
Lambda 本质上是一个闭包对象,而函数指针仅保存函数地址。对于无捕获 Lambda,由于其不包含任何状态信息,编译器可以将其转换为对应的函数指针;而有捕获 Lambda 内部保存了成员数据,调用时依赖具体对象,因此无法转换为普通函数指针。
