Lambda 表达式 (C++11 起)
构造闭包:能够捕获作用域中变量的无名函数对象。
目录 |
[编辑] 语法
[ captures ] <tparams>(可选)(C++20) ( params ) specifiers(可选) exception attr -> ret requires(可选)(C++20) { body }
|
(1) | ||||||||
[ captures ] ( params ) -> ret { body }
|
(2) | ||||||||
[ captures ] ( params ) { body }
|
(3) | ||||||||
[ captures ] { body }
|
(4) | ||||||||
1) 完整声明。
2) const lambda 的声明:以复制捕获的对象在 lambda 体内为 const 。
3) 省略尾随返回类型:闭包的 operator()
的返回类型根据下列规则确定:
|
(C++14 前) |
返回类型从 return 语句推导,如同对于返回类型声明为 auto 的函数。 |
(C++14 起) |
4) 省略参数列表:函数不接收参数,如同参数列表是 ()
。仅若不使用 constexpr 、 mutable 、异常规定、属性或尾随返回类型之一才能使用此形式。
[编辑] 解释
captures | - | 零或更多捕获的逗号分隔列表,可选地以 capture-default 起始。
捕获列表能按如下方式传递(详细描述见下方):
若变量满足下列条件,则 lambda 表达式能使用而不捕获它 若变量满足下列条件,则 lambda 表达式能读取其值而不捕获它
| ||
<tparams>(C++20) | - | 模板形参列表(于角括号中),用于提供名称给泛型 lambda 的模板形参(见后述 ClosureType::operator() )。类似在模板声明中,模板形参列表可以后附可选的 requires 子句,它指定模板实参上的制约。
| ||
params | - | 参数列表,如在具名函数中,除了不允许默认参数 (C++14 前)。若以 auto 为参数类型,则该 lambda 为泛型 lambda 。 (C++14 起)
| ||
specifiers | - | 可选的指定符序列。允许下列指定符:
| ||
exception | - | 为闭包类型的 operator() 提供异常规定或 noexcept 子句 | ||
attr | - | 为闭包类型的 operator() 提供属性指定 | ||
ret | - | 返回类型。若缺失,则由函数的 return 语句所隐含(或若函数不返回任何值则为 void ) | ||
body | - | 函数体 |
Lambda 表达式是纯右值表达式,其类型是唯一的无名非联合非聚合类类型,被称为闭包类型,它(为 ADL 的目的)声明于含有该 lambda 表达式的最小块作用域、类作用域或命名空间作用域。闭包类型有下列成员:
ClosureType::operator()(params)
ret operator()(params) const { body } |
(不使用关键词 mutable) | |
ret operator()(params) { body } |
(使用关键词 mutable) | |
template<template-params> ret operator()(params) { body } |
(C++14 起) (泛型 lambda) |
|
template<template-params> ret operator()(params) { body } |
(C++14 起) (泛型 lambda ,使用关键词 mutable) |
|
在调用时执行 lambda 表达式体。访问变量时,访问其被捕获的副本(对于以复制捕获的实体)或原对象(对于以引用捕获的实体)。除非于 lambda 表达式使用关键词 mutable ,否则函数调用表达式为 const 限定,且以复制捕获的对象从此operator() 的内部不可修改。函数调用运算符决非 volatile 限定且决非虚。
若函数调用运算符满足 constexpr 函数的要求,则它始终是 |
(C++17 起) |
对于 params 中每个类型指定为 // 泛型 lambda , operator() 是有二个形参的模板 auto glambda = [](auto a, auto&& b) { return a < b; }; bool b = glambda(3, 3.14); // ok // 泛型 lambda , operator() 是有一个形参的模板 auto vglambda = [](auto printer) { return [=](auto&&... ts) // 泛型 lambda , ts 是形参包 { printer(std::forward<decltype(ts)>(ts)...); return [=] { printer(ts...); }; // 空型 lambda (不接收参数) }; }; auto p = vglambda([](auto v1, auto v2, auto v3) { std::cout << v1 << v2 << v3; }); auto q = p(1, 'a', 3.14); // 输出 1a3.14 q(); // 输出 1a3.14
|
(C++14 起) |
若 lambda 定义使用显式模板形参列表,则模板形参列表用于 // 泛型 lambda , operator() 是有二个形参的模板 auto glambda = []<class T>(T a, auto&& b) { return a < b; }; // 泛型 lambda , operator() 是有一个形参包的模板 auto f = []<typename ...Ts>(Ts&& ...ts) { return foo(std::forward<Ts>(ts)...); }; |
(C++20 起) |
Lambda 表达式上的异常规定 exception 应用于函数调用运算符或运算符模板。
为了名称查找、确定 this 指针的类型和值,及为访问非静态类成员,闭包类型的函数调用运算符体被认为在 lambda 表达式的环境中。
struct X { int x, y; int operator()(int); void f() { // 下列 lambda 的环境是成员函数 X::f [=]()->int { return operator()(this->x + y); // X::operator()(this->x + (*this).y) // 拥有类型 X* }; } };
ClosureType
的 operator()
不能于友元声明中指名。
悬垂引用
若以引用隐式或显式捕获非引用实体,且在实体的生存期结束后调用闭包的函数调用运算符,则未定义行为发生。 C++ 闭包不通过被捕获的引用延长生存期。
同样适用于被捕获的 this
指针所指向对象的生存期。
ClosureType::operator ret(*)(params)()
(无捕获非泛型 lambda) | ||
using F = ret(*)(params); operator F() const; |
(C++17 前) | |
using F = ret(*)(params); constexpr operator F() const; |
(C++17 起) | |
(无捕获泛型 lambda) | ||
template<template-params> using fptr_t = /*see below*/; template<template-params> operator fptr_t<template-params>() const; |
(C++14 起) (C++17 前) |
|
template<template-params> using fptr_t = /*see below*/; template<template-params> operator fptr_t<template-params>() const; |
(C++17 起) | |
此用户定义转换函数仅若 lambda 表达式的捕获列表为空才得到定义。它是闭包对象的公开、 constexpr (C++17 起) 非虚、非 explicit 、 const noexcept (C++14 起) 成员函数。
泛型无捕获 lambda 拥有用户定义函数转换模板,它拥有与函数调用运算符模板相同的虚设模板形参。若返回类型为空或 auto ,则返回类型以函数模板特化上的返回类型推导获得,也就是以转换函数模板的模板实参推导获得。 void f1(int (*)(int)) {} void f2(char (*)(int)) {} void h(int (*)(int)) {} // #1 void h(char (*)(int)) {} // #2 auto glambda = [](auto a) { return a; }; f1(glambda); // ok f2(glambda); // 错误:不可转换 h(glambda); // ok :调用 #1 因为 #2 不可转换 int& (*fpi)(int*) = [](auto* a)->auto& { return *a; }; // ok |
(C++14 起) |
此转换函数返回的值是指向拥有 C++ 语言链接的函数,而该函数得到调用时,拥有与直接调用闭包对象的函数调用运算符的相同效果。
若函数调用运算符(或对于泛型 lambda 为特化)为 constexpr ,则此函数为 constexpr 。 auto Fwd= [](int(*fp)(int), auto a){return fp(a);}; auto C=[](auto a){return a;}; static_assert(Fwd(C,3)==3);// OK auto NC=[](auto a){ static int s; return a;}; static_assert(Fwd(NC,3)==3); // 错误:因为 s 而没有能为 constexpr 的特化 若闭包对象的 |
(C++17 起) |
ClosureType::ClosureType()
ClosureType() = delete; |
(C++14 前) | |
ClosureType(const ClosureType& ) = default; |
(C++14 起) | |
ClosureType(ClosureType&& ) = default; |
(C++14 起) | |
闭包类型非可默认构造 (DefaultConstructible
) 。闭包类型有被删除的 (C++14 前)无 (C++14 起)默认构造函数。复制构造函数与移动构造函数被隐式声明 (C++14 前)声明为默认化 (C++14 起)并可能按照复制构造函数与移动构造函数的通常规则隐式定义。
ClosureType::operator=(const ClosureType&)
ClosureType& operator=(const ClosureType&) = delete; |
||
闭包类型非可复制赋值 (CopyAssignable
) 。
ClosureType::~ClosureType()
~ClosureType() = default; |
||
析构函数是隐式声明的。
ClosureType::Captures
T1 a; T2 b; |
||
若 lambda 表达式以复制(隐式地以捕获子句 [=]
或显式地以不含字符 & 的捕获任何内容,例如 [a, b, c]
),则闭包类型包含以未指定顺序声明的无名非静态数据成员,它保有所有被如此捕获的实体的副本。
对应无初始化器捕获的数据成员在求值 lambda 表达式时被直接初始化。对应有初始化器捕获者按初始化器的要求初始化(可为复制或直接初始化)。若捕获数组,则数组元素以下标递增顺序直接初始化。数据成员初始化所用顺序是声明所用顺序(它是未指定的)。
每个数据成员的类型是对应被捕获实体的类型,除非实体拥有引用类型(该情况下,到函数的引用被捕获为到被引用函数的左值引用,而到引用的对象被捕获为被引用对象的副本)。
对于以引用捕获(以默认捕获 [&]
或使用字符 & ,例如 [&a, &b, &c]
时)的实体,闭包类型中是否声明附加的数据成员是未指定的,但任何这种附加成员必须满足字面类型 (LiteralType
) (C++17 起)。
Lambda 表达式不允许存在于不求值表达式、模板实参、别名声明、 typedef 声明及函数(或函数模板)声明中函数体和函数默认参数以外的任何位置。 |
(C++20 前) |
[编辑] Lambda 捕获
captures 是零或更多捕获的逗号分隔列表,可选地以 capture-default 开始。仅有的捕获默认是
-
&
(以引用隐式捕获 odr 使用的自动变量)和 -
=
(以复制捕获 odr 使用的自动变量)。
无论存在哪个捕获默认,都能隐式捕获当前对象( *this
)。若隐式捕获,则始终以引用捕获之,即使捕获默认是 =
。
captures 中合法捕获的语法是
identifier | (1) | ||||||||
identifier ...
|
(2) | ||||||||
identifier initializer | (3) | (C++14 起) | |||||||
& identifier
|
(4) | ||||||||
& identifier ...
|
(5) | ||||||||
& identifier initializer
|
(6) | (C++14 起) | |||||||
this
|
(7) | ||||||||
* this
|
(8) | (C++17 起) | |||||||
若捕获默认是 &
,则后继简单捕获必须不以 &
开始。
struct S2 { void f(int i); }; void S2::f(int i) { [&]{}; // OK :默认以引用捕获 [&, i]{}; // OK :以值捕获,除了 i 以引用捕获 [&, &i] {}; // 错误:默认以引用时的以引用捕获 [&, this] {}; // OK :等价于 [&] [&, this, i]{}; // OK :等价于 [&, i] }
若捕获默认是 =
,则后继简单捕获必须以 &
开始或是 *this
(C++17 起) 或 this
(C++20 起) 。
struct S2 { void f(int i); }; void S2::f(int i) { [=]{}; // OK :默认以复制捕获 [=, &i]{}; // OK :以复制捕获,除了 i 以引用捕获 [=, *this]{}; // C++17 前:错误:非法语法 // C++17 起: OK :以复制捕获外围 S2 [=, this] {}; // C++20 前:错误: = 为默认时的 this // C++20 起: OK :同 [=] }
任何捕获只可以出现一次:
struct S2 { void f(int i); }; void S2::f(int i) { [i, i] {}; // 错误: i 重复 [this, *this] {}; // 错误: "this" 重复 (C++17) }
只有定义于块作用域或默认成员初始化器中的 lambda 表达式能拥有无初始化器的捕获默认。对于这种 lambda 表达式,触及作用域定义为直至并包含最内层的外围函数(及其参数)的外围作用域集。这包含嵌套块作用域及外围 lambda 的作用域,若此 lambda 为嵌套。
任何无初始化器的捕获(异于 this
捕获中的 identifier 用通常非限定名称查找在 lambda 的触及作用域中查找。查找结果必须是声明于触及作用域中,有自动存储期的变量。变量(或 this
)被显式捕获。
带初始化器的捕获,行动如同它声明并显示捕获以类型 auto 声明的变量,变量的声明性区域是 lambda 表达式体(即它不在其初始化器的作用域中),除了:
这用于捕获仅移动类型,以例如 x = std::move(x) 的捕获 int x = 4; auto y = [&r = x, x = x + 1]()->int { r += 2; return x * x; }(); // 更新 ::x 为 6 并初始化 y 为 25 。 |
(C++14 起) |
若捕获列表拥有捕获默认,且不显式捕获外围对象(如 this
或 *this
)或任何自动变量,则它隐式捕获之,若
- lambda 体 odr 使用变量或
this
指针
void f(int, const int (&)[2] = {}) {} // #1 void f(const int&, const int (&)[1]) {} // #2 void test() { const int x = 17; auto g0 = [](auto a) { f(x); }; // ok :调用 #1 ,不捕获 x auto g1 = [=](auto a) { f(x); }; // C++14 中不捕获 x , C++17 中捕获 x // 捕获能被优化掉 auto g2 = [=](auto a) { int selector[sizeof(a) == 1 ? 1 : 2] = {}; f(x, selector); // ok :此为依赖表达式,故 x }; auto g3 = [=](auto a) { typeid(a + x); // 捕获 x ,不管 a + x 是否为不求值运算数 }; } |
(C++14 起) |
若 lambda 体 odr 使用以复制捕获的实体,则它访问闭包类型的成员。若它不 odr 使用该实体,则访问是到原对象的:
void f(const int*); void g() { const int N = 10; [=]{ int arr[N]; // 非 odr 使用:指代 g 的 const int N f(&N); // odr 使用:导致 N 被捕获(以复制) // &N 是闭包对象的成员 N 的地址,而非 g 的 N 的 }(); }
#include <iostream> auto make_function(int& x) { return [&]{ std::cout << x << '\n'; }; } int main() { int i = 3; auto f = make_function(i); // x 于 f 中的使用直接绑定到 i i = 5; f(); // OK :打印 5 }
在 lambda 体内,在任何有自动存储期的变量上的任何 decltype 使用如同将它捕获并 odr 使用,尽管 decltype 自身不是 odr 使用且不发生实际捕获:
void f3() { float x, &r = x; [=] { // x 与 r 不被捕获(出现作 decltype 运算数中不是 odr 使用) decltype(x) y1; // y1 拥有 float 类型 decltype((x)) y2 = y1; // y2 拥有 float const& 类型,因为此 lambda // 非 mutable 且 x 是左值 decltype(r) r1 = y1; // r1 拥有 float& 类型(不考虑变换) decltype((r)) r2 = y2; // r2 拥有 float const& 类型 }; }
Lambda 所捕获(隐式或显示)的任何实体为 lambda 表达式所 odr 使用(从而嵌套 lambda 的隐式捕获触发外围 lambda 的隐式捕获)。
所有隐式捕获的变量必须声明于 lambda 的触及作用域中。
若 lambda 捕获外围对象(用 this
或 *this
),则最接近的外围函数必须是非静态成员函数,或者该 lambda 必须在默认成员初始化器中:
struct s2 { double ohseven = .007; auto f() { // 下列二个 lambda 的最接近外围函数 return [this] { // 以引用捕获外围 s2 return [*this] { // 以复制捕获外围 s2 (C++17) return ohseven; // OK } }(); } auto g() { return []{ // 无捕获 return [*this]{}; // 错误: *this 未为外层 lambda 表达式所捕获 }(); } };
若 lambda 表达式(或泛型 lambda 的函数调用运算符) ODR 使用 this
或任何有自动存储期的变量,则它必须为 lambda 表达式所捕获。
void f1(int i) { int const N = 20; auto m1 = [=] { int const M = 30; auto m2 = [i] { int x[N][M]; // N 与 M 未被 odr 使用 // ( ok 因为它们未被捕获) x[0][0] = i; // i 为 m2 显式捕获 // 并为 m1 隐式捕获 }; }; struct s1 // f1() 中的局部类 { int f; void work(int n) // 非静态成员函数 { int m = n * n; int j = 40; auto m3 = [this, m] { auto m4 = [&, j] { // 错误: j 未为 m3 所捕获 int x = n; // 错误: n 为 m4 隐式捕获 // 但不为 m3 所捕获 x += m; // ok : m 为 m4 捕获 // 且为 m3 显式捕获 x += i; // 错误: i 在触及作用域之外 // (该作用域终于 work() ) x += f; // ok : this 为 m4 隐式捕获 // 且为 m3 显式捕获 }; }; } }; }
不能捕获类成员而不带初始化器(如上提及,捕获列表中仅容许变量):
class S { int x = 0; void f() { int i = 0; // auto l1 = [i, x]{ use(i, x); }; // 错误: x 非变量 auto l2 = [i, x=x]{ use(i, x); }; // OK ,复制捕获 i = 1; x = 1; l2(); // 调用 use(0,0) auto l3 = [i, &x=x]{ use(i, x); }; // OK ,引用捕获 i = 2; x = 2; l3(); // 调用 use(1,2) } };
当 lambda 用隐式以复制捕获捕获成员时,它不复制该成员变量:成员变量 m
的使用被处理成表达式 (*this).m ,而 *this 始终隐式以引用捕获:
class S { int x = 0; void f() { int i = 0; auto l1 = [=]{ use(i, x); }; // 捕获 i 的副本与 this 指针的副本 i = 1; x = 1; l1(); // 调用 use(0,1) ,如同 i 以复制而 x 以引用 auto l2 = [i, this]{ use(i, x); }; // 同上,令之为显式 i = 2; x = 2; l2(); // 调用 use(1,2),如同 i 以复制而 x 以引用 auto l3 = [&]{ use(i, x); }; // 以引用捕获 i ,并捕获 this 指针的副本 i = 3; x = 2; l3(); // 调用 use(3,2) ,如同 i 与 x 均以引用 auto l4 = [i, *this]{ use(i, x); }; // 制造 *this 的副本,包含 x 的副本 i = 4; x = 4; l4(); // 调用 use(3,2) ,如同 i 与 x 均以复制 } };
若 lambda 表达式出现于默认参数,则它不能显式或隐式捕获任何变量。
不能捕获匿名联合体的成员。
若嵌套 lambda m2
捕获亦为立即外围的 lambda m1
所捕获的某变量,则 m2
的捕获按下列方式传递:
- 若外围 lambda
m1
以复制捕获,则m2
捕获m1
的闭包类型的非静态成员,而非原变量或this
。 - 若外围 lambda
m1
以引用捕获,则m2
捕获原变量或this
。
#include <iostream> int main() { int a = 1, b = 1, c = 1; auto m1 = [a, &b, &c]() mutable { auto m2 = [a, b, &c]() mutable { std::cout << a << b << c << '\n'; a = 4; b = 4; c = 4; }; a = 3; b = 3; c = 3; m2(); }; a = 2; b = 2; c = 2; m1(); // 调用 m2() 并打印 123 std::cout << a << b << c << '\n'; // 打印 234 }
[编辑] 示例
此示例演示如何传递 lambda 给泛型算法,以及 lambda 表达式所产生的对象能如何存储于 std::function 对象。
#include <vector> #include <iostream> #include <algorithm> #include <functional> int main() { std::vector<int> c = {1, 2, 3, 4, 5, 6, 7}; int x = 5; c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; }), c.end()); std::cout << "c: "; std::for_each(c.begin(), c.end(), [](int i){ std::cout << i << ' '; }); std::cout << '\n'; // 闭包的类型不能指名,但可用 auto 提及 // C++14 起, lambda 能拥有自身的默认参数 auto func1 = [](int i = 6) { return i + 4; }; std::cout << "func1: " << func1() << '\n'; // 同所有可调用对象,闭包能捕获于 std::function // (这可能带来不必要的开销) std::function<int(int)> func2 = [](int i) { return i + 4; }; std::cout << "func2: " << func2(6) << '\n'; }
输出:
c: 5 6 7 func1: 10 func2: 10
[编辑] 缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 1891 | C++14 | 闭包有被删除的默认构造函数和隐式的复制/移动构造函数 | 无默认及默认化的复制/移动 |
CWG 1722 | C++14 | 无捕获 lambda 的转换函数有未指定的异常规定 | 转换函数为 noexcept |
[编辑] 参阅
auto 指定符 | 指定表达式所定义的类型(C++11) |
(C++11) |
包装任何类型的有指定函数调用签名的可调用对象 (类模板) |