c++ 中的 Lambda
介绍
Lambda 表达式(Lambda Expression),在 C++11 中被引入,是一种可以就地定义、没有名字、能捕获外部变量的匿名函数对象(闭包)。
语法
这是一个完整的 Lambda 表达式:
1 | [capture] (params) mutable exception attribute -> ret { body } |
- 捕获列表
[capture]:定义 Lambda 可以访问外部作用域的哪些变量,以及如何访问(按值还是按引用)。 - 参数列表
(params)(可选):与普通函数一致。 - 说明符
mutable(可选):默认情况下,按值捕获的变量在内部是const的,加上mutable允许修改这些副本。 - 异常声明
noexcept(可选):指定是否抛出异常。 - 返回类型
-> type(可选):通过尾置返回类型指定。 - 函数体
{}:函数具体的实现逻辑。
Lambda 表达式是以类(Class)形式定义,我们以一个最复杂的形式介绍:
1 | int x = 10; |
其 Class 展开:
1 | class __lambda_unique_id { |
捕获模式
Lambda 中的捕获拥有多种模式。
| 捕获形式 | 说明 |
|---|---|
[] | 不捕获。Lambda 只能访问全局变量或静态变量。 |
[x] | 按值捕获 x。内部拥有 x 的副本,修改副本不影响外部。 |
[&x] | 按引用捕获 x。内部修改直接影响外部变量。 |
[=] | 隐式按值捕获。捕获函数体内用到的所有外部变量。 |
[&] | 隐式按引用捕获。捕获函数体内用到的所有外部变量。 |
[=, &x] | 默认按值捕获,但 x 必须按引用捕获。 |
[this] | 捕获当前类的指针,允许 Lambda 访问类的成员变量和成员函数。 |
generalized capture 带初始化的捕获
从 C++14 起,捕获列表 [capture] 支持自定义变量,但必须拥有初值,加上 mutable 后可变,生命周期跟随 Lambda。
也可以通过引用的方式定义一个变量,用于给外部变量一个别名。
参数列表
从 C++14 起,支持用 auto 声明参数,会生成 [[#泛型 Lambda]] 表达式。
如果不将参数传递给 lambda,并且其声明不包含 mutable,且没有后置返回值类型,则可以省略空括号.
返回类型
通常编译器可以自动推导返回类型,注意当 Lambda 中多个返回类型不一样且未指定返回类型,会产生编译错误。
泛型 Lambda
使用 auto 声明参数类型是,会构造泛型 Lambda。
1 | auto add = [](auto a, auto b) { return a + b; }; |
其 Class 展开:
1 | class __lambda_unique_id { |
额,看不懂没关系,只用注意到它是以 template<typename T1, typename T2> 模版形式定义就可以了。
Lambda 的递归
在 C++ 中,Lambda 的递归比普通函数要复杂一些。核心矛盾在于:Lambda 在定义完成之前,它的名字(变量名)在函数体内是不可见的。
比如对于一个 错误实现:
1 | auto fib = [](int n) { if(n<=1) return n; return fib(n-1); } |
因为推导 fib 的类型需要知道它的返回类型,而确定返回类型又需要调用 fib,陷入了死循环。
所以接下来介绍主要的三种实现方式。
std::function
注意会有额外性能开销
1 | std::function<int(int)> fib = [&](int n) { return (n <= 1) ? n : fib(n - 1) + fib(n - 2); }; |
这是最常用的方法。通过显式指定 std::function 类型,提前确定了 Lambda 的签名,使得内部可以识别这个名称。
注意不建议使用该方式,存在类型擦除和动态内存分配的开销,且无法进行内联优化,性能略低。
泛型 Lambda 自传递
1 | auto fib = [](auto self, int n) -> int { |
其 Class 展开:
1 | class __lambda_fib { |
零开销。编译器可以完全内联,性能等同于普通递归函数。
只是语法可能略显奇怪。
使用辅助函数 std::visit 或自定义 y_combinator
为了解决上一个方法 多传一个参数 的尴尬,可以写一个辅助包装类(通常称为 y_combinator)。
1 | template<class F> |
优点在于调用语法非常亲切。
缺点在于比较麻烦。
C++23
C++23 引入了 “Deducing this” 语法,允许 Lambda 直接显式访问自身,彻底解决了递归问题。
1 | auto fib = [](this auto&& self, int n) -> int { |
