Lambda 表达式 (C++11 起)
构造闭包:能够捕获作用域中的变量的无名函数对象。
语法
[ 捕获 ] ( 形参 ) lambda说明符 约束(可选) { 函数体 }
|
(1) | ||||||||
[ 捕获 ] { 函数体 }
|
(2) | (C++23 前) | |||||||
[ 捕获 ] lambda说明符 { 函数体 }
|
(2) | (C++23 起) | |||||||
[ 捕获 ] <模板形参> 约束(可选)( 形参 ) lambda说明符 约束(可选) { 函数体 }
|
(3) | (C++20 起) | |||||||
[ 捕获 ] <模板形参> 约束(可选) { 函数体 }
|
(4) | (C++20 起) (C++23 前) | |||||||
[ 捕获 ] <模板形参> 约束(可选) lambda说明符 { 函数体 }
|
(4) | (C++23 起) | |||||||
()
。解释
捕获 | - | 包含零或更多个捕获符的逗号分隔列表,可以 默认捕获符(capture-default) 起始。
有关捕获符的详细描述,见下文。 如果变量满足下列条件,那么 lambda 表达式在使用它前不需要先捕获: 如果变量满足下列条件,那么 lambda 表达式在读取它的值前不需要先捕获:
| ||||
<模板形参> | - | (角括号中的)模板形参列表,用于为泛型 lambda 提供各模板形参的名字(见下文的 闭包类型::operator() )。与在模板声明中相似,模板形参列表可以后附 requires 子句,它指定各模板实参上的约束。
模板形参列表不能为空(不允许 | ||||
形参 | - | 形参列表,如在具名函数中。 | ||||
lambda说明符 | - | 由 说明符、 异常说明、 属性 和 尾随返回类型 按顺序组成,每个组分均非必需 | ||||
说明符 | - | 可选的说明符的序列。不提供说明符时复制捕获的对象在 lambda 体内是 const 的。可以使用下列说明符:
| ||||
异常说明 | - | 为闭包类型的 operator() 提供动态异常说明或 (C++20 前) noexcept 说明符
| ||||
属性 | - | 为闭包类型的函数调用运算符或运算符模板的类型提供属性说明。这样指定的任何属性均属于函数调用运算符或运算符模板的类型,而非其自身。(例如不能使用 [[noreturn]] )
| ||||
尾随返回类型 | - | -> 返回类型 ,其中 返回类型 指定返回类型。如果没有 尾随返回类型,那么闭包的 operator() 的返回类型从 return 语句推导,如同对于声明返回类型为 auto 的函数的推导一样。
| ||||
约束 | - | (C++20 起)向闭包类型的 operator() 添加约束
| ||||
函数体 | - | 函数体 |
当以 |
(C++14 起) |
lambda 表达式是纯右值表达式,它的类型是独有的无名非联合非聚合类类型,被称为闭包类型(closure type),它(对于 ADL 而言)在含有该 lambda 表达式的最小块作用域、类作用域或命名空间作用域声明。闭包类型有下列成员:
闭包类型::operator()(形参)
返回类型 operator()(形参) const { 函数体 } |
(未使用关键词 mutable) | |
返回类型 operator()(形参) { 函数体 } |
(使用了关键词 mutable) | |
template<模板形参> 返回类型 operator()(形参) const { 函数体 } |
(C++14 起) (泛型 lambda) |
|
template<模板形参> 返回类型 operator()(形参) { 函数体 } |
(C++14 起) (泛型 lambda,使用了关键词 mutable) |
|
当被调用时,执行 lambda 表达式的函数体。当访问变量时,访问的是其被捕获的副本(对于以复制捕获的实体)或原对象(对于以引用捕获的实体)。除非 lambda 表达式中使用了关键词 mutable,否则函数调用运算符或运算符模板会被 const 限定,且无法从这个 operator() 的内部修改以复制捕获的对象。函数调用运算符或运算符模板始终不被 volatile 限定且始终非虚。
如果函数调用运算符或任意给定的运算符模板特化满足针对 constexpr 函数的要求,那么它始终是 constexpr 的。如果关键词 |
(C++17 起) |
如果在 lambda 表达式中使用关键词 |
(C++20 起) |
对于 形参 中每个类型指定为 // 泛型 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 表达式上的异常说明 异常说明 应用于函数调用运算符或运算符模板。
对于名字查找、确定 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) // this 拥有类型 X* }; } };
闭包类型
的 operator()
不能在友元声明中指名。
悬垂引用
如果以引用隐式或显式捕获非引用实体,而在该实体的生存期结束之后调用闭包对象的函数调用运算符或运算符模板特化,那么会发生未定义行为。C++ 的闭包并不延长以引用捕获的对象的生存期。
这同样适用于由 this
捕获对当前 *this 对象。
闭包类型::operator 返回类型(*)(形参)()
无捕获的非泛型 lambda |
||
using F = 返回类型(*)(形参); operator F() const noexcept; |
(C++17 前) | |
using F = 返回类型(*)(形参); constexpr operator F() const noexcept; |
(C++17 起) | |
无捕获的泛型 lambda |
||
template<模板形参> using fptr_t = /*见下文*/; template<模板形参> |
(C++14 起) (C++17 前) |
|
template<模板形参> using fptr_t = /*见下文*/; template<模板形参> |
(C++17 起) | |
只有在 lambda 表达式的捕获符列表为空时才定义这个用户定义转换函数。它是闭包对象的公开、constexpr、 (C++17 起)非虚、非 explicit、const noexcept 成员函数。
如果函数调用运算符(或对于泛型 lambda 为其特化)是立即函数,那么此函数是立即函数。 |
(C++20 起) |
泛型无捕获 lambda 拥有一个用户定义的转换函数模板,它具有与函数调用运算符模板相同的虚设模板形参列表。如果它的返回类型为空或 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); // 错误:因为 static s 而不能为 constexpr 的特化 如果闭包对象的 |
(C++17 起) |
闭包类型::闭包类型()
闭包类型() = delete; |
||
闭包类型() = default; |
(C++20 起) (仅当未指定任何捕获时) |
|
闭包类型(const 闭包类型& ) = default; |
||
闭包类型(闭包类型&& ) = default; |
||
闭包类型非可默认构造 (DefaultConstructible) 。闭包类型没有默认构造函数。 |
(C++20 前) |
如果没有指定 捕获,那么闭包类型拥有预置的默认构造函数。否则,它没有默认构造函数(这包含有 默认捕获符(capture-default) 的情况,即使它实际上没有捕获任何变量)。 |
(C++20 起) |
闭包类型::operator=(const 闭包类型&)
闭包类型& operator=(const 闭包类型&) = delete; |
(C++20 前) | |
闭包类型& operator=(const 闭包类型&) = default; 闭包类型& operator=(闭包类型&&) = default; |
(C++20 起) (仅当未指定任何捕获时) |
|
闭包类型& operator=(const 闭包类型&) = delete; |
(C++20 起) (其他情况) |
|
复制赋值运算符被定义为弃置的(且未声明移动赋值运算符)。闭包类型非可复制赋值 (CopyAssignable) 。 |
(C++20 前) |
如果没有指定 捕获,那么闭包类型拥有预置的复制赋值运算符和预置的移动赋值运算符。否则,它拥有弃置的复制赋值运算符(这包含有 默认捕获符 的情况,即使它实际上没有捕获任何变量)。 |
(C++20 起) |
闭包类型::~闭包类型()
~闭包类型() = default; |
||
析构函数是隐式声明的。
闭包类型::捕获
T1 a; T2 b; |
||
如果 lambda 表达式以复制(隐式地以捕获子句 [=]
或显式地以不含字符 & 的捕获符,例如 [a, b, c]
)捕获了任何内容,那么闭包类型包含保有所有被如此捕获的实体的副本的无名非静态数据成员,它们以未指明的顺序声明。
如果数据成员对应的捕获符没有初始化器,那么它们在求值 lambda 表达式时被直接初始化。如果有初始化器,那么按其初始化器的要求初始化(可为复制或直接初始化)。如果捕获了数组,那么各数组元素以下标递增顺序直接初始化。初始化各数据成员所用的顺序是它们的声明顺序(即未指明)。
每个数据成员的类型是其对应被捕获实体的类型,除非实体拥有引用类型(此时到函数的引用被捕获为到被引用函数的左值引用,而到对象的引用被捕获为被引用对象的副本)。
对于以引用捕获(以默认捕获符 [&]
或使用了字符 &,例如 [&a, &b, &c]
)的实体,闭包类型中是否声明额外的数据成员是未指明的,但任何这种附加成员必须满足字面类型 (LiteralType) (C++17 起)。
不允许在不求值表达式、模板实参、别名声明、typedef 声明,以及函数(或函数模板)声明中除了函数体和函数的默认实参以外的任何位置中出现 lambda 表达式。
|
(C++20 前) |
Lambda 捕获
捕获 是一个含有零或更多个捕获符的逗号分隔列表,可以 默认捕获符 开始。默认捕获符只有
-
&
(以引用隐式捕获被使用的自动变量)和 -
=
(以复制隐式捕获被使用的自动变量)。
当出现任一默认捕获符时,都能隐式捕获当前对象(*this)。当它被隐式捕获时,始终被以引用捕获,即使默认捕获符是 =
。当默认捕获符为 =
时,*this 的隐式捕获被弃用。 (C++20 起)
捕获 中单独的捕获符的语法是
标识符 | (1) | ||||||||
标识符 ...
|
(2) | ||||||||
标识符 初始化器 | (3) | (C++14 起) | |||||||
& 标识符
|
(4) | ||||||||
& 标识符 ...
|
(5) | ||||||||
& 标识符 初始化器
|
(6) | (C++14 起) | |||||||
this
|
(7) | ||||||||
* this
|
(8) | (C++17 起) | |||||||
... 标识符 初始化器
|
(9) | (C++20 起) | |||||||
& ... 标识符 初始化器
|
(10) | (C++20 起) | |||||||
当默认捕获符是 &
时,后继的简单捕获符不能以 &
开始。
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 表达式,其可达作用域(reaching scope)定义为其最内层的外围函数(及其形参)内(包含自身)的外围作用域的集合。这其中包含各个嵌套的块作用域,以及当此 lambda 为嵌套的 lambda 时也包含其各个外围 lambda 的作用域。
(除了 this
捕获符之外的)任何无初始化器的捕获符中的 标识符 会使用通常的无限定名字查找在 lambda 的可达作用域中查找。查找结果必须是在可达作用域中声明的且具有自动存储期的变量,或对应变量满足这种要求的结构化绑定 (C++20 起)。该实体被显式捕获。
带有初始化器的捕获符的行为如同它声明并显式捕获一个以类型 auto 声明的变量,该变量的声明区是 lambda 表达式体(即它不在其初始化器的作用域中),但:
这可以用于以像 x = std::move(x) 这样的捕获符捕获仅可移动的类型。 这也可以使通过 const 引用进行捕获成为可能,比如以 &cr = std::as_const(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 式使用的自动变量,或对应变量拥有自动存储期的结构化绑定 (C++20 起),那么在以下情况下,它隐式捕获之:
- lambda 体 ODR 式使用了该实体
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); // f 中对 x 的使用直接绑定到 i i = 5; f(); // OK;打印 5 }
在带默认捕获符 =
的 lambda 体内,任何可捕获对实体的类型为如同它被捕获(从而若 lambda 非 mutable
则通常会加上 const 限定),即使该实体在不求值运算数中且未被捕获(例如在 decltype
中):
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 的函数调用运算符的一个实例化) (C++14 起) 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 式使用 // (它们可以不被捕获) 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'; std::cout << "模仿递归 lambda 调用:\n斐波那契数:"; auto nth_fibonacci = [](int n) { std::function<int(int,int,int)> fib = [&](int a, int b, int n) { return n ? fib(a + b, a, n - 1) : b; }; return fib(1, 0, n); }; for (int i{1}; i != 9; ++i) { std::cout << nth_fibonacci(i) << ", "; } std::cout << "\n另一种 lambda 递归方案:\n斐波那契数:"; auto nth_fibonacci2 = [](int n) { auto fib = [](auto self, int a, int b, int n) -> int { return n ? self(self, a + b, a, n - 1) : b; }; return fib(fib, 1, 0, n); }; for (int i{1}; i != 9; ++i) { std::cout << nth_fibonacci2(i) << ", "; } }
输出:
c: 5 6 7 func1: 10 func2: 10 模仿递归 lambda 调用: 斐波那契数:1, 1, 2, 3, 5, 8, 13, 21, 另一种 lambda 递归方案: 斐波那契数:1, 1, 2, 3, 5, 8, 13, 21,
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 974 | C++11 | lambda 表达式的形参列表中不能有默认实参 | 允许 |
CWG 975 | C++11 | 只有在 lambda 体含有单条 return 语句时 才推导闭包的 operator() 的返回类型
|
如同对于 C++14 返回 auto 的函数一般推导 |
CWG 1722 | C++11 | 无捕获的 lambda 的转换函数的异常说明未指明 | 转换函数为 noexcept |
CWG 1891 | C++11 | 闭包带有弃置的默认构造函数和隐含的复制/移动构造函数 | 无默认及预置的复制/移动 |
参阅
auto 说明符(C++11)
|
指定从表达式推导的类型 |
(C++11) |
包装具有指定函数调用签名的任意可复制构造类型的可调用对象 (类模板) |