lambda 表达式 (C++11 起)

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 
 

构造闭包:能够捕获作用域中的变量的无名函数对象。

语法

没有显式模板形参的 lambda 表达式(可以不泛型)

[捕获 ] 前属性 (可选) (形参列表 ) 说明符 (可选) 异常 (可选)
后属性 (可选) 尾随类型 (可选) 约束 (可选) { 函数体 }
(1)
[捕获 ] { 函数体 } (2) (C++23 前)
[捕获 ] 前属性 (可选) 尾随类型 (可选) { 函数体 } (2) (C++23 起)
[捕获 ] 前属性 (可选) 异常
后属性 (可选) 尾随类型 (可选) { 函数体 }
(3) (C++23 起)
[捕获 ] 前属性 (可选) 说明符 异常 (可选)
后属性 (可选) 尾随类型 (可选) { 函数体 }
(4) (C++23 起)

有显式模板形参的 lambda 表达式(必然泛型) (C++20 起)

[捕获 ] <模板形参 > 模板约束 (可选) 前属性 (可选) (形参列表 ) 说明符 (可选)
异常 (可选) 后属性 (可选) 尾随类型 (可选) 约束 (可选) { 函数体 }
(1)
[捕获 ] <模板形参 > 模板约束 (可选) { 函数体 } (2) (C++23 前)
[捕获 ] <模板形参 > 模板约束 (可选)
前属性 (可选) 尾随类型 (可选) { 函数体 }
(2) (C++23 起)
[捕获 ] <模板形参 > 模板约束 (可选) 前属性 (可选) 异常
后属性 (可选) 尾随类型 (可选) { 函数体 }
(3) (C++23 起)
[捕获 ] <模板形参 > 模板约束 (可选) 前属性 (可选) 说明符 异常 (可选)
后属性 (可选) 尾随类型 (可选) { 函数体 }
(4) (C++23 起)
1) 带形参列表的 lambda 表达式。
2-4) 不带形参列表的 lambda 表达式。
2) 最简形式,无法应用后属性
3,4) 只有在有出现说明符 或异常 的情况下才能应用后属性

解释

捕获 - 包含零或更多个捕获符的逗号分隔列表,可以默认捕获符 起始。

有关捕获符的详细描述,见下文

如果变量满足下列条件之一,那么 lambda 表达式在使用它前不需要先捕获:

  • 该变量是非局部变量,或具有静态或线程局部存储期(此时无法捕获该变量)。
  • 该变量是以常量表达式初始化的引用。

如果变量满足下列条件之一,那么 lambda 表达式在读取它的值前不需要先捕获:

  • 该变量具有 const 而非 volatile 的整型或枚举类型,并已经用常量表达式初始化。
  • 该变量是 constexpr 的且没有 mutable 成员。
模板形参 - 非空的模板形参列表,用于为泛型 lambda 提供各模板形参的名字(见下文的 闭包类型::operator())。
模板约束 - 模板形参 添加约束。

如果模板约束 以属性说明符序列结尾,那么该序列中的属性会被视为前属性 中的属性。

(C++23 起)
前属性 - (C++23 起) 适用于闭包类型的 operator()属性说明符序列(因此可以使用 [[noreturn]] 属性)。
形参列表 - 闭包类型的 operator()形参列表

它可以有一个显式对象形参

(C++23 起)
说明符 - 可包含以下说明符的序列,每个说明符在序列中最多允许出现一次。
说明符 效果
mutable 允许函数体 修改复制捕获的对象,以及调用它们的非 const 成员函数。
  • 存在显式对象形参时不能使用。
(C++23 起)
constexpr
(C++17 起)
显式指定 operator()constexpr 函数
  • 如果 operator()满足针对 constexpr 函数的所有要求,那么即使没有指定 constexproperator() 也会是 constexpr 的。
consteval
(C++20 起)
指定 operator()立即函数
  • 不能同时指定 constevalconstexpr
static
(C++23 起)
指定 operator()静态成员函数
  • 不能同时指定 staticmutable
  • 捕获 不为空,或者存在显式对象形参时不能使用。
异常 - 为闭包类型的 operator() 提供动态异常说明 (C++20 前) noexcept 说明符
后属性 - 适用于闭包类型的 operator() 的类型的属性说明序列(因此不能使用 [[noreturn]] 属性)。
尾随类型 - -> 返回类型,其中返回类型 指定返回类型。
约束 - (C++20 起)向闭包类型的 operator() 添加约束
函数体 - lambda 表达式的函数体。


当以 auto 为形参类型或显式提供模板形参列表 (C++20 起)时,该 lambda 是泛型 lambda

(C++14 起)

变量 __func__函数体 的开头隐式定义,它的语义可以参考这里

闭包类型

lambda 表达式是纯右值表达式,它的类型是独有的无名非联合体聚合体类类型,被称为闭包类型,它(对于 实参依赖查找 而言)声明于含有该 lambda 表达式的最小块作用域、类作用域或命名空间作用域。

当且仅当捕获 为空时,闭包类型是结构化类型。

(C++20 起)

闭包类型有下列成员,它们不能显式实例化,被显式特化,或 (C++14 起)友元声明中指名:

闭包类型::operator()(形参)

返回类型 operator()(形参) { 函数体 }
(static 和 const 可能会出现,见下文)
template<模板形参>
返回类型 operator()(形参) { 函数体 }
(C++14 起)
(泛型 lambda,static 和 const 可能会出现,见下文)

当被调用时,执行 lambda 表达式的函数体。当访问变量时,访问的是它被捕获的副本(对于按复制捕获的实体)或原对象(对于按引用捕获的实体)。

operator() 的形参列表在提供了形参列表 时是该形参列表,否则为空。

operator() 的返回类型是尾随类型 中指定的类型。

如果没有提供尾随类型,那么 operator() 的返回类型会被自动推导[1]

除非 lambda 的说明符中使用了关键词 mutable,或者存在显式对象形参 (C++23 起),否则 operator() 的 cv 限定符都会是 const,并且无法从这个 operator() 的内部修改按复制捕获的对象。operator() 始终非虚,并且不能包含 volatile 限定符。

如果 operator() 满足针对 constexpr 函数的要求,那么它始终是 constexpr 的。如果在 lambda 的说明符中使用了关键词 constexpr,那么它也是 constexpr 的。

(C++17 起)

如果在 lambda 的说明符中使用了关键词 consteval,那么 operator()立即函数

(C++20 起)

如果在 lambda 的说明符中使用了关键词 static,那么 operator()静态成员函数

如果形参列表 有一个显式对象形参,那么 operator()显式对象成员函数

(C++23 起)


对于形参 中每个类型指定为 auto 的形参,以它们的出现顺序向模板形参 中添加一个虚设的模板形参。当虚设的模板形参所对应的形参 中的函数形参是函数形参包时,它可以是形参包

// 泛型 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)...);
        // 零元 lambda (不接受形参)
        return [=] { printer(ts...); };
    };
};
 
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 定义使用了显式的模板形参列表,该列表会用于 operator()。对于形参 中的每个类型指定为 auto 的形参,都会向模板形参列表追加一个虚设的模板形参:

// 泛型 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 表达式上的异常说明异常 应用于 operator()

对于名字查找、确定 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 对象的生存期。

  1. 尽管函数返回类型推导在 C++14 中引入,但是 C++11 中已经可以使用相应规则推导 lambda 返回类型。

闭包类型::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<模板形参>

operator fptr_t<模板形参>() const noexcept;
(C++14 起)
(C++17 前)
template<模板形参> using fptr_t = /* 见下文 */;

template<模板形参>

constexpr operator fptr_t<模板形参>() const noexcept;
(C++17 起)

只有在 lambda 表达式的捕获符列表为空且没有显式对象形参 (C++23 起)时才定义这个用户定义转换函数。它是闭包对象的公开、constexpr、 (C++17 起)非虚、非显式、const noexcept 成员函数。

如果函数调用运算符(或对于泛型 lambda 的函数调用运算符特化)是立即函数,那么此函数是立即函数

(C++20 起)

泛型无捕获 lambda 拥有一个用户定义的转换函数模板,它具有与 operator() 模板相同的虚设模板形参列表。

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++ 语言连接的函数的指针,调用该函数的效果与在默认构造的闭包类型实例上调用闭包类型的函数调用运算符的效果相同。

(C++14 前)

这个转换函数(模板)返回一个指向具有 C++ 语言连接的函数的指针,调用该函数的效果与下列效果相同:

  • 对于非泛型 lambda,在默认构造的闭包类型实例上调用闭包类型的 operator()
  • 对于泛型 lambda,在默认构造的闭包类型实例上调用 operator() 模板的泛型 lambda 对应特化。
(C++14 起)
(C++23 前)

这个转换函数(模板):

  • operator() 是静态的情况下返回一个指向具有 C++ 语言连接的该 operator() 的指针。
  • 否则返回一个指向具有 C++ 语言连接的函数的指针,调用该函数的效果与下列效果相同:
    • 对于非泛型 lambda,在默认构造的闭包类型实例上调用闭包类型的 operator()
    • 对于泛型 lambda,在默认构造的闭包类型实例上调用 operator() 模板的泛型 lambda 对应特化。
(C++23 起)


如果函数调用运算符(或对于泛型 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 的特化

如果闭包对象的 operator() 具有无抛出异常说明,那么此函数返回的指针具有指向 noexcept 函数的指针类型。

(C++17 起)

闭包类型::闭包类型()

闭包类型() = default;
(C++20 起)
(仅当未指定任何捕获时)
闭包类型(const 闭包类型&) = default;
闭包类型(闭包类型&&) = default;

闭包类型非可默认构造 (DefaultConstructible) 。闭包类型没有默认构造函数。

(C++20 前)

如果没有指定捕获,那么闭包类型拥有预置的默认构造函数。否则,它没有默认构造函数(这包含有 默认捕获符  的情况,即使它实际上没有捕获任何变量)。

(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 捕获

捕获 是一个含有零或更多个捕获符 的逗号分隔列表,可以默认捕获符 开始。捕获列表定义了可以从 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 起)
1) 简单的按复制捕获
2) 作为包展开的简单的按复制捕获
3)初始化器的按复制捕获
4) 简单的按引用捕获
5) 作为包展开的简单的按引用捕获
6) 带初始化器的按引用捕获
7) 当前对象的简单的按引用捕获
8) 当前对象的简单的按复制捕获
9) 初始化器为包展开的按复制捕获
10) 初始化器为包展开的按引用捕获

当默认捕获符是 & 时,后继的简单捕获符不能以 & 开始。

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)
 
    [i] (int i) {};   // 错误:形参和捕获的名字相同
}

只有定义于块作用域或默认成员初始化器中的 lambda 表达式能拥有默认捕获符或无初始化器的捕获符。对于这种 lambda 表达式,它的可达作用域(reaching scope)定义为它最内层的外围函数(及它的形参)内(包含自身)的外围作用域的集合。这其中包含各个嵌套的块作用域,以及当此 lambda 为嵌套的 lambda 时也包含它的各个外围 lambda 的作用域。

(除了 this 捕获符之外的)任何无初始化器的捕获符中的标识符 ,都使用通常的无限定名字查找在 lambda 的可达作用域中查找。查找结果必须是在可达作用域中声明的且具有自动存储期的变量,或对应变量满足这种要求的结构化绑定 (C++20 起)。该实体被显式捕获

带有初始化器的捕获符的行为如同声明并显式捕获以 auto 类型说明符声明并拥有相同初始化器的变量,该变量的作用域是 lambda 表达式体(即不在初始化器的作用域内),但:

  • 如果按复制捕获,则因而引入的闭包对象的非静态数据成员和该变量将被视为引用同一对象;
    • 换言之,源变量并不实际存在,而经由 auto 的类型推导和初始化均应用到该非静态数据成员;
  • 如果按引用捕获,那么引用变量的生存期在闭包对象的生存期结束时结束。

这可以用于,以 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 起),则如果潜在求值的表达式内的表达式(包括在使用非静态类成员前添加的隐式 this->)指名了该实体,则捕获列表隐式捕获该实体。

就确定隐式捕获的目的而言,标准认为 typeid 决不使其操作数不求值。

即使实体仅在 lambda 函数体的舍弃语句内被指名,它们也可能被隐式捕获。

(C++17 起)
void f(int, const int (&)[2] = {}) {}   // #1
void f(const int&, const int (&)[1]) {} // #2
 
struct NoncopyableLiteralType
{
    constexpr explicit NoncopyableLiteralType(int n) : n_(n) {}
    NoncopyableLiteralType(const NoncopyableLiteralType&) = delete;
 
    int n_;
};
 
void test()
{
    const int x = 17;
 
    auto l0 = []{ f(x); };           // OK:调用 #1,不捕获 x
    auto g0 = [](auto a) { f(x); };  // 同上
 
    auto l1 = [=]{ f(x); };          // OK:(P0588R1 起)捕获 x 并调用 #1
                                     // 捕获能被优化掉
    auto g1 = [=](auto a) { f(x); }; // 同上
 
    auto ltid = [=]{ typeid(x); };   // OK:(P0588R1 起)捕获 x
                                     // 尽管不求值 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 是否为不求值操作数
    };
 
    constexpr NoncopyableLiteralType w{42};
    auto l4 = []{ return w.n_; };      // OK:不 ODR 使用 w,捕获是不必要的
    // auto l5 = [=]{ return w.n_; };  // 错误:需要按复制捕获 w
}

如果 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
    }();
}

如果 lambda ODR 使用了按引用捕获的引用,那么它使用原引用所指代的对象,而非被捕获的引用自身:

#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 表达式在默认实参中出现,那么它不能显式或隐式捕获任何内容,除非所有捕获都带有初始化器,并满足可以在默认实参中出现的表达式的约束条件 (C++14 起)

void f2()
{
    int i = 1;
 
    void g1( int = [i] { return i; }() ); // 错误:有捕获内容
    void g2( int = [i] { return 0; }() ); // 错误:有捕获内容
    void g3( int = [=] { return i; }() ); // 错误:有捕获内容
 
    void g4( int = [=] { return 0; }() );       // OK:无捕获
    void g5( int = [] { return sizeof i; }() ); // OK:无捕获
 
    // C++14
    void g6( int = [x = 1] { return x; }() ); // OK:1 可以在默认实参中出现
    void g7( int = [x = i] { return x; }() ); // 错误:i 不能在默认实参中出现
}

不能捕获匿名联合体的成员。只能以复制捕获位域

如果嵌套的 lambda m2 捕获了也被它的直接外围 lambda m1 所捕获的实体,那么以如下方式将 m2 的捕获进行变换:

  • 如果外围 lambda m1 按复制捕获,那么 m2 捕获 m1 的闭包类型的非静态数据成员,而非原变量或 *this;如果 m1mutable,那么认为该非静态数据成员有 const 限定。
  • 如果外围 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 有捕获,那么函数调用表达式的显式对象形参(如果存在)的类型只能是以下之一:

  • 闭包类型。
  • 公开且无歧义地从闭包类型派生的类类型。
  • 到可有 cv 限定性的这种类型的引用类型。
struct C 
{
    template<typename T>
    C(T);
};
 
void func(int i) 
{
    int x = [=](this auto&&) { return i; }();  // OK
    int y = [=](this C) { return i; }();       // 错误
    int z = [](this C) { return 42; }();       // OK
 
    auto lambda = [n = 42] (this auto self) { return n; };
    using Closure = decltype(lambda);
    struct D : private Closure {
        D(Closure l) : Closure(l) {}
        using Closure::operator();
        friend Closure;
    };
    D{lambda}(); // 错误
}
(C++23 起)

注解

功能特性测试宏 标准 功能特性
__cpp_lambdas 200907L (C++11) Lambda 表达式
__cpp_generic_lambdas 201304L (C++14) 泛型 lambda 表达式
201707L (C++20) 泛型 lambda 表达式的显式模板形参列表
__cpp_init_captures 201304L (C++14) Lambda 初始化捕获
201803L (C++20) 允许 lambda 初始化捕获中的包展开
__cpp_capture_star_this 201603L (C++17) 按值捕获 *this,如 [=, *this]
__cpp_constexpr 201603L (C++17) constexpr 的 lambda
__cpp_static_call_operator 202207L (C++23) 无捕获 lambda 的静态 operator()

缺陷报告 P0588R1 稍微更改了隐式 lambda 捕获的规则。到 2023-10 为止,一些主要实现没有完全实现该 DR,从而有时仍在使用检测 ODR 使用的旧规则。

P0588R1 前的旧规则

如果捕获符列表具有默认捕获符,且未显式(以 this*this)捕获它的外围对象,或任何在 lambda 体内可 ODR 使用的自动存储期变量,或对应变量拥有自动存储期的结构化绑定 (C++20 起),那么在实体符合下列条件时捕获列表会隐式 捕获该实体:

  • 为依赖泛型 lambda 模板形参的表达式之内的潜在求值表达式中所指名,或
(C++14 起)

示例

此示例演示如何传递 lambda 给泛型算法,以及 lambda 表达式所产生的对象能如何存储于 std::function 对象。

#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
 
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';
 
    constexpr int fib_max {8};
    std::cout << "模仿递归 lambda 调用:\n斐波那契数:";
    auto nth_fibonacci = [](int n)
    {
        std::function<int(int, int, int)> fib = [&](int n, int a, int b)
        {
            return n ? fib(n - 1, a + b, a) : b;
        };
        return fib(n, 0, 1);
    };
 
    for (int i{1}; i <= fib_max; ++i)
        std::cout << nth_fibonacci(i) << (i < fib_max ? ", " : "\n");
 
    std::cout << "另一种 lambda 递归方案:\n斐波那契数:";
    auto nth_fibonacci2 = [](auto self, int n, int a = 0, int b = 1) -> int
    {
        return n ? self(self, n - 1, a + b, a) : b;
    };
 
    for (int i{1}; i <= fib_max; ++i)
        std::cout << nth_fibonacci2(nth_fibonacci2, i) << (i < fib_max ? ", " : "\n");
 
#ifdef __cpp_explicit_this_parameter
    std::cout << "C++23 的 lambda 递归方案:\n";
    auto nth_fibonacci3 = [](this auto self, int n, int a = 0, int b = 1) -> int
    {
         return n ? self(n - 1, a + b, a) : b;
    };
 
    for (int i{1}; i <= fib_max; ++i)
        std::cout << nth_fibonacci3(i) << (i < fib_max ? ", " : "\n");
#endif
}

可能的输出:

c: 5 6 7
func1: 10
func2: 10
模仿递归 lambda 调用:
斐波那契数:0, 1, 1, 2, 3, 5, 8, 13
另一种 lambda 递归方案:
斐波那契数:0, 1, 1, 2, 3, 5, 8, 13

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 974 C++11 lambda 表达式的形参列表中不能有默认实参 可以有
CWG 1048
(N3638)
C++11 只能对仅包含一个 return 语句的 lambda 体推导返回类型 改进返回类型推导
CWG 1249 C++11 不明确外围非 mutable 的 lambda 所捕获的成员是否为 const 认为它为 const
CWG 1557 C++11 未指定闭包类型的转换函数的返回函数类型的语言链接 它具有 C++ 语言链接
CWG 1607 C++11 lambda 表达式可以在函数和函数模板签名中出现 已禁止
CWG 1612 C++11 可以捕获匿名联合体的成员 已禁止
CWG 1722 C++11 无捕获的 lambda 的转换函数的异常说明未指明 转换函数为 noexcept
CWG 1772 C++11 lambda 体内 __func__ 的语义不明确 它指代闭包类的 operator()
CWG 1780 C++14 不明确泛型 lambda 的闭包类型的成员是否可以被显式实例化或被显式特化 两者都不允许
CWG 1891 C++11 闭包带有弃置的默认构造函数和隐含的复制/移动构造函数 无默认及预置的复制/移动
CWG 1937 C++11 未指明调用转换函数的返回值的效果与调用哪个对象的 operator() 相同 与调用默认构造的闭包类型
实例的 operator() 相同
CWG 1973 C++11 闭包类型的 operator() 的形参列表可以指代尾随类型 中的形参列表 只能指代形参列表
CWG 2011 C++11 对于按引用捕获的引用,未指明该捕获符的标识符表示的是哪个实体 表示的是原来引用的实体
CWG 2095 C++11 按复制捕获到函数的右值引用的行为不明确 使之明确
CWG 2211 C++11 未指明捕获与形参的名字相同时的行为 此时程序非良构
CWG 2358 C++14 在默认实参中的 lambda 表达式不能有任何捕获,
即使它们都被可以在默认实参内出现的表达式初始化
这种带捕获的 lambda 表达式
可以在默认实参中出现
CWG 2509 C++17 说明符序列里每个声明符可以多次出现 只能各出现最多一次
CWG 2561 C++23 有显式对象形参的 lambda 表达式可能拥有到不想要的函数指针类型的转换函数 没有这种转换函数
CWG 2881 C++23 有显式对象形参的 operator() 能对继承非公开或有歧义的派生类实例化 使之非良构
P0588R1 C++11 隐式 lambda 捕获的规则检测 ODR 使用 简化了规则

参阅

auto 说明符(C++11) 指定从表达式推导的类型
(C++11)
任意可复制构造的可调用对象的可复制包装
(类模板)
任意可调用对象的仅移动包装,支持给定调用签名中的限定符
(类模板)

外部链接

嵌套函数 - 一个函数定义于另一个(外围)函数之内。