Lambda 表达式
注意 :考虑到算法竞赛的实际情况,本文将不会全面研究语法,只会讲述在算法竞赛中可能会应用到的部分.
本文语法参照 C++11 标准,其他高版本的标准语法视情况提及并会特别标注.
Lambda 表达式
Lambda 表达式因数学中的 𝜆λ 演算得名,直接对应于其中的 lambda 抽象.编译器在编译时会根据语法生成一个匿名的 函数对象,以捕获的变量作为其成员,参数和函数体用于实现
operator() 重载.
函数对象(Function Object)
函数对象是一种类对象,一般通过重载 operator() 实现,所以能像函数一样调用.相较于使用普通的函数,函数对象有很多优点,例如可以保存状态,可以作为参数传递给其他函数等.
以下是 lambda 的一种语法:
---|---
Lambda 表达式本身是一个类,展开后如以下形式:
---|---
空的 capture 可以隐式转换为函数指针,例如:
---|---
下面我们分别对语法中的各部分进行介绍.
### statement 函数体
函数体与普通函数函数体类似,除了能访问参数和全局变量等,还可访问 捕获 的变量.
### capture 捕获子句
lambda 以 capture 子句开头,它指定哪些变量被捕获,捕获列表可为空,或指定捕获方式:有 `&` 符号前缀的变量通过 [引用](../reference/) 访问,没有该前缀的变量通过值访问.
我们也可以使用默认捕获模式,捕获 Lambda 中提及的所有变量:`&` 表示捕获到的所有变量都通过引用访问,`=` 表示捕获到的所有变量都通过值访问.
在默认捕获之后,仍然可以为特定的变量 **显式** 指定捕获模式.
如果需要引用访问外部变量 `a`,并通过值访问外部变量 `b`,那么以下捕获子句都可以做到:
* `[&a, b]`
* `[b, &a]`
* `[&, b]`
* `[b, &]`
* `[=, &a]`
同时捕获列表也可以用于声明变量,类型由初始化器推导,类似于使用 `auto` 声明变量.
以下是一些常见的例子:
---|---
generalized capture 带初始化的捕获(C++14)
自 C++14 起,capture 不仅可以用来捕获外部变量,还可用于声明新的变量并初始化,例如:
---|---
定义新的变量不可以省略初始值,变量的类型由初始值的类型决定,相当于:
---|---
以下是错误的写法:
---|---
初始化值也可以是外部变量,例如:
---|---
val 也可以是一个引用类型,可以引用一个外部变量,通过这种方式可以为通过引用捕获的外部变量取个别名,例如:
---|---
捕获外部变量和定义新变量可以同时使用.
如果你想在 Lambda 表达式内修改 capture 中定义的新变量,需要使用 `mutable` 关键字,如果是引用则不需要,例如:
---|---
详见 mutable 可变规范.
在 capture 中定义的变量的生命周期跟随 Lambda 表达式的接收方,在以上几个示例中为变量 𝑓f,因为 Lambda 本身其实是一个类,capture 中的所有内容都是这个类的
private 成员变量,例如:
---|---
### parameters 参数列表
大多数情况下类似于函数的参数列表,例如:
---|---
这将打印出 x 数组从大到小排序后的结果.
由于 parameters 参数列表 是可选的,如果不将参数传递给 lambda,并且其声明不包含 mutable,且没有后置返回值类型,则可以省略空括号.
使用 auto 声明的参数
C++14 后,若参数使用 auto 声明类型,那么会构造一个 泛型 Lambda 表达式.
显式对象形参(C++23)
C++23 起,显式对象形参 可以在 lambda 的参数中使用.
---|---
### mutable 可变规范
使得函数体可以修改通过值捕获的变量.
---|---
在执行完 by_value() 后,by_value 的捕获成员 a 为 1,但外部的变量 a 依然为 0. 而在执行完 by_ref() 后,外部 a 的值变为 1.
return-type 返回类型
用于指定 lambda 表达式的返回类型.如果省略,则返回类型将被自动推断(行为与用 auto 声明返回值的函数一致).
多个 return 语句且推导类型不一致时,将产生编译错误.
---|---
### 泛型 Lambda(C++14)
使用 `auto` 作为参数类型,可以构造泛型 lambda.
---|---
在 cpp insights 中可以观察到编译器生成的 lambda 类定义:
---|---
`add` 两个参数声明均使用了 `auto`,对应为 `add_lambda` 类的 `operator()` 函数模板的两个模板参数 `T` 和 `U`.
### Lambda 中的递归
先来看一个编译失败的例子:
---|---
我们这里尝试在捕获列表中捕获 𝑑𝑓𝑠dfs,但是有一个问题,𝑑𝑓𝑠dfs
的类型为
auto,要等待等号右边的类型推导完成后才会推导出 𝑑𝑓𝑠dfs 的类型,而 Lambda 要捕获 𝑑𝑓𝑠dfs
就必须要确定 𝑑𝑓𝑠dfs
的类型后才能创建它的引用变量,好,这会陷入了一个套娃过程.
怎么解决这个问题呢?
- 显式指定 𝑑𝑓𝑠dfs
的类型,可以使用
std::function替代.
修改如上代码为:
---|---
不建议使用 [`std::function`](../new/#stdfunction) 实现的递归
`std::function` 的类型擦除通常需要分配额外内存,同时间接调用带来的寻址操作会进一步降低性能.
在 [Benchmark](https://quick-bench.com/q/U5qf_dHHKsSyVU83jmt0p_U541c) 测试中,使用 Clang 17 编译器,libc++ 作为标准库,`std::function` 实现比 lambda 实现的递归慢了约 2.5 倍.
测试代码
---|---
- 不通过捕获的方式获取 𝑑𝑓𝑠dfs
,而是通过函数传参的方式.
修改如上代码为:
---|---
`auto self`、`auto& self` 和 `auto&& self` 的区别:
`auto& self` 和 `auto&& self` 理论上都只会使用 88 个字节(指针的大小)用作传参,不会发生其他的拷贝.具体要看编译器对 Lambda 的实现方式和对应的优化. 而使用 `auto self` 会发生对象拷贝,拷贝的大小取决于捕获列表中的元素,因为它们都是这个 Lambda 类中的私有成员变量.
1. 可以通过手动展开 Lambda 类,或使用类似写法,这样可以直接声明 𝑑𝑓𝑠dfs 的类型.
修改如上代码为:
---|---
- 如果 lambda 没有捕获任何变量,我们也可以利用函数指针.
如果 lambda 没有捕获任何变量,那么它可以隐式转换为函数指针.同时 lambda 此时也可以声明为 static,函数指针类型也可以声明为 static.如此依赖,lambda 可以不需要捕获就能访问函数指针,从而实现递归.
示例
---|---
### Lambda 表达式的应用
#### 作为标准库算法的 Predicate(谓词)
从大到小排序:
---|---
使用 std::find_if 查找第一个大于 3 的元素:
---|---
#### 控制中间变量的生命周期
在算法竞赛中,我们会遇到这样的场景:一个变量的初始化需要使用之前声明的变量,其初始化过程又生成占用空间较大的中间变量.
我们希望能尽快析构这些中间变量,以降低内存消耗.此时,我们可以使用 lambda 来控制这些中间变量的生命周期.
---|---
相较于使用块作用域,lambda 可以允许我们使用返回值,使得代码更加简洁;相较于函数,我们不需要额外起名和声明被捕获的各种参数,使得代码更加紧凑.
参考文献
本页面最近更新: 2026/2/26 03:56:39,更新历史 发现错误?想一起完善?在 GitHub 上编辑此页! 本页面贡献者:Tiphereth-A, c0nstexpr, c-forrest, CCXXXI, CoderOJ, Great-designer, hly1204, huanhuanonly, Persdre, shuzhouliu, ZnPdCo, zyzzyh 本页面的全部内容在CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用