Lambda 表达式 (C++11 起)

来自cppreference.com
< cpp‎ | language

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

目录

[编辑] 语法

[ captures ] <tparams>(可选)(C++20) ( params ) specifiers(可选) exception attr -> ret { body } (1)
[ captures ] ( params ) -> ret { body } (2)
[ captures ] ( params ) { body } (3)
[ captures ] { body } (4)

1) 完整声明。

2) const lambda 的声明:不能修改以复制捕获的对象。

3) 省略尾随返回类型:闭包的 operator() 的返回类型根据下列规则确定:

(C++14 前)

返回类型从 return 语句推导,如同对于返回类型声明为 auto 的函数。

(C++14 起)

4) 省略参数列表:函数不接收参数,如同参数列表是 () 。仅若不使用 constexpr 、 mutable 、异常规定、属性或尾随返回类型之一才能使用此形式。

[编辑] 解释

captures - 零或更多捕获的逗号分隔列表,可选地以 capture-default 起始。

捕获列表能按如下方式传递(详细描述见下方):

  • [a,&b] 其中 a 以复制捕获而 b 以引用捕获。
  • [this] 以引用捕获当前对象( *this
  • [&] 以引用捕获所有用于 lambda 体内的自动变量,并以引用捕获当前对象,若存在
  • [=] 以复制捕获所有用于 lambda 体内的自动变量,并以引用捕获当前对象,若存在
  • [] 不捕获

若变量不拥有自动存储期(即它不是局部变量,或它为静态或为线程局域)或若它不被 odr 使用于 lambda 体,则不捕获它就能使用。

<tparams>(C++20) - 模板形参列表(于角括号中),用于向泛型 lambda 的模板形参提供名称(见后述 ClosureType::operator()
params - 参数列表,如在具名函数中,除了不允许默认参数 (C++14 前)若将 auto 用作参数类型,则该 lambda 为泛型 lambda (C++14 起)
specifiers - 可含有下列指定符:
  • mutable :允许 body 修改以复制捕获的参数,及调用其非 const 成员函数
  • constexpr :显式指定函数调用运算符为 constexpr 函数。此指定符不存在时,若函数调用运算符恰好满足所有 constexpr 函数要求,则它也会是 constexpr
(C++17 起)
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)

在调用时执行 lambda 表达式体。访问变量时,访问其被捕获的副本(对于以复制捕获的实体)或原对象(对于以引用捕获的实体)。除非于 lambda 表达式使用关键词 mutable ,否则函数调用表达式为 const 限定,且以复制捕获的对象从此operator() 的内部不可修改。函数调用运算符决非 volatile 限定且决非虚。

若函数调用运算符满足 constexpr 函数的要求,则它始终是 constexpr 。若关键词 constexpr 用于 lambda 声明,则它亦为 constexpr 。

(C++17 起)

对于 params 中每个类型指定为 auto 的参数,以出现顺序添加一个虚设模板形参到 template-params 。虚设模板形参可以是参数包,若对应的 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

ClosureTypeoperator() 不能被显式实例化或显式特化。

(C++14 起)

若 lambda 定义使用显式模板形参列表,则模板形参列表用于 operator() 。对于 params 中每个类型指定为 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 表达式上的异常规定 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*
        };
    }
};

ClosureTypeoperator() 不能于友元声明中指名。

悬垂引用

若以引用隐式或显式捕获非引用实体,且在实体的生存期结束后调用闭包的函数调用运算符,则未定义行为发生。 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 的特化

若闭包对象的 operator() 拥有不抛出异常规定,则此函数返回的指针拥有指向 noexcept 函数的指针类型。

(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 声明及函数(或函数模板)声明中函数体和函数默认参数以外的任何位置。

[编辑] 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)
1) 简单以复制捕获
2) 作为包展开的简单以复制捕获
3)初始化器的以复制捕获
4) 简单以引用捕获
5) 作为包展开的简单引用捕获
6) 带初始化器的以引用捕获
7) 当前对象的简单以引用捕获
8) 当前对象的简单以复制捕获

若捕获默认是 & ,则后继简单捕获必须不以 & 开始。

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 )或任何自动变量,则它隐式捕获之,若

  • 或者,变量或 this 在依赖泛型 lambda 参数的表达式内的潜在求值的表达式中被指名:
void f(int, const int (&)[2] = {}) {} // #1
void f(const int&, const int (&)[1]) {} // #2
void test()
{
    const int x = 17;
    auto g1 = [](auto a) { f(x); }; // ok :调用 #1 ,不捕获 x
    auto g2 = [=](auto a) {
            int selector[sizeof(a) == 1 ? 1 : 2] = {};
            f(x, selector); // ok :是依赖表达式,故捕获 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 的
    }();
}
若 lambda odr 使用以引用捕获的引用,则它使用原引用所指代的对象,而非被捕获的引用自身:
#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 ),则最接近的外围函数必须是非静态成员函数:

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
}


[编辑] 示例

此示例演示 (a) 如何传递 lambda 给泛型算法; (b) 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 提及
    auto func1 = [](int i) { return i + 4; };
    std::cout << "func1: " << func1(6) << '\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)
包装任何类型的有指定函数调用签名的可调用对象
(类模板) [编辑]