C++对lambda表达式的支持是从C++11版本开始,后续版本又有一些增强,本文仅根据C++11标准讨论lambda表达式的原理和应用。
定义
构造闭包:能够捕获作用域中的变量的无名函数对象。
关于C++闭包可参考:https://www.cnblogs.com/Aion/p/3449756.html
语法
[ 捕获 ] ( 形参 ) lambda说明符 约束(可选) { 函数体 } | (1) | |
[ 捕获 ] { 函数体 } | (2) | (C++23 前) |
[ 捕获 ] lambda说明符 { 函数体 } | (2) | (C++23 起) |
[ 捕获 ] <模板形参> 约束(可选) ( 形参 ) lambda说明符 约束(可选) { 函数体 } | (3) | (C++20 起) |
[ 捕获 ] <模板形参> 约束(可选) { 函数体 } | (4) | (C++20 起) (C++23 前) |
[ 捕获 ] <模板形参> 约束(可选) lambda说明符 { 函数体 } | (4) | (C++23 起) |
- 1) 完整声明。
- 2) 省略形参列表:函数不接收实参,如同形参列表是 ()。
- 3) 与 1) 相同,但指定泛型 lambda 并显式提供模板形参列表。
- 4) 与 2) 相同,但指定泛型 lambda 并显式提供模板形参列表。
解释
- 捕获:包含零或更多个捕获符的逗号分隔列表,可以 默认捕获符(capture-default) 起始。 有关捕获符的详细描述,见下文。 如果变量满足下列条件,那么 lambda 表达式在使用它前不需要先捕获:
- 该变量是非局部变量,或具有静态或线程局部存储期(此时无法捕获该变量),或者
- 该变量是以常量表达式初始化的引用。
如果变量满足下列条件,那么 lambda 表达式在读取它的值前不需要先捕获: - 该变量具有 const 而非 volatile 的整型或枚举类型,并已经用常量表达式初始化,或者
- 该变量是 constexpr 的且没有 mutable 成员。
- 形参:形参列表,如在具名函数中。
- lambda说明符:由 说明符、异常说明、属性 和 尾随返回类型 按前述顺序组成,每个组分均非必需
- 说明符:可选的说明符的序列。不提供说明符时复制捕获的对象在 lambda 体内是 const 的。可以使用下列说明符:
- mutable:允许 函数体 修改复制捕获的对象,以及调用它们的非 const 成员函数
- 异常说明:为闭包类型的 operator() 提供动态异常说明或 (C++20 前) noexcept 说明符
- 属性:为闭包类型的函数调用运算符或运算符模板的类型提供属性说明。这样指定的任何属性均属于函数调用运算符或运算符模板的类型,而非其自身。(例如不能使用 [[noreturn]])
- 尾随返回类型:-> 返回类型,其中 返回类型 指定返回类型。如果没有 尾随返回类型,那么闭包的 operator() 的返回类型从 return 语句推导,如同对于声明返回类型为 auto 的函数的推导一样。
- 函数体:函数体
- <模板形参>:(角括号中的)模板形参列表,用于为泛型 lambda 提供各模板形参的名字(见下文的 闭包类型::operator())。与在模板声明中相似,模板形参列表可以后附 requires 子句,它指定各模板实参上的约束。
模板形参列表不能为空(不允许 <>)。 - 约束:(C++20 起)向闭包类型的 operator() 添加约束
原理
定义中说的很清楚,lambda构造了一个闭包——能够捕获作用域中的变量的无名函数对象。
首先:lambda表达式的值是一个函数对象,所以可以像使用函数对象一样使用lambda表达式构建的对象;
其次:这个函数对象捕获了其被创建时的作用域中的变量,可以在operator()实现中使用这些捕获的变量;
第三:这个函数对象无名,个人理解为其所构造的对象的类型无名,类型无法在其他地方使用,而对象本身可以用一个function对象保存。
函数对象是C++一个很基础的应用,无需多说,无名也好理解,编译器自动为我们构造了一个匿名的类并构造了这个类的一个对象,我们无需知道类名,因为与上下文关联,也就无需(不能)重复使用这个类。剩下的中点就是捕获作用域中的变量了。
语法中**[ 捕获 ]**这部分就用来说明闭包对象以何种方式捕获哪些作用域中的变量了。
关于lambda定义的闭包类型
lambda 表达式是纯右值表达式,它的类型是独有的无名非联合非聚合类类型,被称为闭包类型(closure type),它在含有该 lambda 表达式的最小块作用域、类作用域或命名空间作用域声明。闭包类型有下列成员,它们不能被显式实例化,被显式特化,或 (C++14 起)在友元声明中指名:
- 闭包类型::operator()(形参)
- 闭包类型::operator 返回类型(*)(形参)()
- 闭包类型::闭包类型()
- 闭包类型::~闭包类型()
- 闭包类型::捕获
Lambda 捕获
捕获 是一个含有零或更多个捕获符的逗号分隔列表,可以 默认捕获符 开始。默认捕获符只有
- &(以引用隐式捕获被使用的自动变量)和
- =(以复制隐式捕获被使用的自动变量)。
当出现任一默认捕获符时,都能隐式捕获当前对象(*this)。如果隐式捕获它,那么会始终以引用捕获,即使默认捕获符是 =。
捕获 中单独的捕获符的语法是
标识符 | (1) | |
标识符 ... | (2) | |
标识符 初始化器 | (3) | (C++14 起) |
& 标识符 | (4) | |
& 标识符 ... | (5) | |
& 标识符 初始化器 | (6) | (C++14 起) |
this | (7) | |
* this | (8) | (C++17 起) |
... 标识符 初始化器 | (9) | (C++20 起) |
& ... 标识符 初始化器 | (9) | (C++20 起) |
- 简单的以复制捕获
- 作为包展开的简单的以复制捕获
- 带初始化器的以复制捕获
- 简单的以引用捕获
- 作为包展开的简单的以引用捕获
- 带初始化器的以引用捕获
- 当前对象的简单的以引用捕获
- 当前对象的简单的以复制捕获
- 初始化器为包展开的以复制捕获
- 初始化器为包展开的以引用捕获
捕获列表的限制和说明:
- 当默认捕获符是 & 时,后继的简单捕获符不能以 & 开始。
- 当默认捕获符是 = 时,后继的简单捕获符必须以 & 开始,或者为 *this (C++17 起) 或 this (C++20 起)。
- 任何捕获符只可以出现一次,并且名字不能与形参相同:
- **只有定义于块作用域或默认成员初始化器中的 lambda 表达式能拥有默认捕获符或无初始化器的捕获符。**对于这种 lambda 表达式,其可达作用域(reaching scope)定义为其最内层的外围函数(及其形参)内(包含自身)的外围作用域的集合。这其中包含各个嵌套的块作用域,以及当此 lambda 为嵌套的 lambda 时也包含其各个外围 lambda 的作用域。(除了 this 捕获符之外的)任何无初始化器的捕获符中的 标识符 会使用通常的无限定名字查找在 lambda 的可达作用域中查找。查找结果必须是在可达作用域中声明的且具有自动存储期的变量,或对应变量满足这种要求的结构化绑定 (C++20 起)。该实体被显式捕获。
- 以不带有初始化器的捕获符不能捕获类成员(如上提及,捕获符列表中只能有变量)(但可以以默认捕获符隐式捕获):
- 如果 lambda 表达式在默认实参中出现,那么它不能显式或隐式捕获任何内容,除非所有捕获都带有完整表达式可以在默认实参中出现的初始化器 (C++14 起):
- 当 lambda 用隐式的以复制捕获捕获某个成员时,它并不产生该成员变量的副本:对成员变量 m 的使用被处理成表达式 (*this).m,而 *this 始终被隐式以引用捕获:
- 如果 lambda 表达式在默认实参中出现,那么它不能显式或隐式捕获任何内容,除非所有捕获都带有完整表达式可以在默认实参中出现的初始化器 (C++14 起):
- 不能捕获匿名联合体的成员。只能以复制捕获位域。
- 如果嵌套的 lambda m2 捕获了也被其直接外围 lambda m1 所捕获的实体,那么以如下方式将 m2 的捕获进行变换:
- 如果外围 lambda m1 以复制捕获,那么 m2 捕获 m1 的闭包类型的非静态数据成员,而非原变量或 *this;如果 m1 非 mutable,那么认为该非静态数据成员有 const 限定。
- 如果外围 lambda m1 以引用捕获,那么 m2 捕获原变量或 *this。
应用举例
附 后续版本中lambda表达式的变化
- 从C++14开始,可以自定义捕获变量并赋值
- 从C++17开始,支持以复制方式捕获当前对象 *this
- 从C++20开始,当默认捕获符为 = 时,*this 的隐式捕获被弃用。
- 从C++20开始,lambda表达式开始支持模板以支持泛型
- 从C++23开始,支持省略形参列表的情况下指定lambda说明符
参考
https://en.cppreference.com/w/cpp/language/lambda
https://www.cnblogs.com/Aion/p/3449756.html
https://blog.csdn.net/weixin_43055404/article/details/103299156