跳转至

# Lambda 表达式

注意：考虑到算法竞赛的实际情况，本文将不会全面研究语法，只会讲述在算法竞赛中可能会应用到的部分．

本文语法参照 C++11 标准，其他高版本的标准语法视情况提及并会特别标注．

## Lambda 表达式

Lambda 表达式因数学中的 𝜆 λ 演算得名，直接对应于其中的 lambda 抽象．编译器在编译时会根据语法生成一个匿名的 函数对象，以捕获的变量作为其成员，参数和函数体用于实现 `operator()` 重载．

函数对象（Function Object）

函数对象是一种类对象，一般通过重载 `operator()` 实现，所以能像函数一样调用．相较于使用普通的函数，函数对象有很多优点，例如可以保存状态，可以作为参数传递给其他函数等．

以下是 lambda 的一种语法：

    1
    [capture] (parameters) mutable -> return-type {statement}

Lambda 表达式本身是一个类，展开后如以下形式：

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    class Lambda_1 {
     private:
      Lambda_1() : capture-list(init-value) { }

     public:
      return-type operator()(parameters) const { statement }

     private:
      mutable capture-list
    };

空的 capture 可以隐式转换为函数指针，例如：

    1
    void (*f)(int, int) = [](int, int) -> void {};

下面我们分别对语法中的各部分进行介绍．

### statement 函数体

函数体与普通函数函数体类似，除了能访问参数和全局变量等，还可访问 捕获 的变量．

### capture 捕获子句

lambda 以 capture 子句开头，它指定哪些变量被捕获，捕获列表可为空，或指定捕获方式：有 `&` 符号前缀的变量通过 引用 访问，没有该前缀的变量通过值访问．

我们也可以使用默认捕获模式，捕获 Lambda 中提及的所有变量：`&` 表示捕获到的所有变量都通过引用访问，`=` 表示捕获到的所有变量都通过值访问．

在默认捕获之后，仍然可以为特定的变量 显式 指定捕获模式．

如果需要引用访问外部变量 `a`，并通过值访问外部变量 `b`，那么以下捕获子句都可以做到：

- `[&a, b]`
- `[b, &a]`
- `[&, b]`
- `[b, &]`
- `[=, &a]`

同时捕获列表也可以用于声明变量，类型由初始化器推导，类似于使用 `auto` 声明变量．

以下是一些常见的例子：

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    int a = 0;
    auto f0 = []() { return a * 9; };   // Error, 无法访问 'a'
    auto f1 = [a]() { return a * 9; };  // OK, 'a' 被值「捕获」
    auto f2 = [&a]() { return a++; };   // OK, 'a' 被引用「捕获」
    auto f3 = [v = a + 1]() {
      return v + 1;
    };  // OK, 使用初始化器声明变量 v，类型与 a 相同

    // 注意，使用引用捕获时，请保证被调用时 a 没有被销毁
    auto b = f2();  // f2 从捕获列表里获得 a 的值，无需通过参数传入 a

#### generalized capture 带初始化的捕获（C++14）

自 C++14 起，capture 不仅可以用来捕获外部变量，还可用于声明新的变量并初始化，例如：

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    auto f1 = [val = 520]() {
      return val;
    };  // OK, 定义 val 类型为 int，初始值为 520，返回值类型 int

    auto f2 = [val = 520LL]() {
      return val;
    };  // OK, 定义 val 类型为 long long，初始值为 520，返回值类型 long long

    auto f3 = [val = "520"]() {
      return val;
    };  // OK, 定义 val 类型为 const char*，初始值为 "520"，返回值类型 const char*

    auto f4 = [val = "520"s]() {
      return val;
    };  // OK, C++14 起，需要 using namespace std; 或 using namespace std::literals;
        // 定义 val 类型为 std::string，初始值为 std::string("520")，返回值类型
        // std::string

    auto f5 = [val = std::string("520")]() {
      return val;
    };  // OK, 定义 val 类型为 std::string，初始值为 std::string("520")，返回值类型
        // std::string

    auto f6 = [val = std::vector<int>(3, 6)]() {
      return val;
    };  // OK, 定义 val 类型为 std::vector<int>，大小为 3，元素填充 6，返回值类型
        // std::vector<int>

    auto f7 = [val = 520]() -> int {
      return val;
    };  // OK, 定义 val 类型为 int，初始值为 520，返回值类型 int

    auto f8 = [val = 520]() -> long long {
      return val;
    };  // OK, 定义 val 类型为 int，初始值为 520，返回值类型 long long

定义新的变量不可以省略初始值，变量的类型由初始值的类型决定，相当于：

    1
    auto val = init-value;

以下是错误的写法：

    1
    2
    auto f = [val]() { return val; };  // Error: 'val' was not declared in this
                                       // scope, identifier "val" is undefined

初始化值也可以是外部变量，例如：

    1
    2
    3
    int value = 520;
    auto f = [val = value]() { return val; };
    std::cout << f();  // Output: 520

`val` 也可以是一个引用类型，可以引用一个外部变量，通过这种方式可以为通过引用捕获的外部变量取个别名，例如：

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    int value = 520;

    auto f = [&val = value]() {
      return val;
    };  // OK, 定义 val 类型为 int&，返回值类型 int，相当于 int& val = value;

    std::cout << f() << '\n';  // Output: 520

    value = 1314;

    std::cout << f() << '\n';  // Output: 1314

捕获外部变量和定义新变量可以同时使用．

如果你想在 Lambda 表达式内修改 capture 中定义的新变量，需要使用 `mutable` 关键字，如果是引用则不需要，例如：

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    int value = 520;

    {
      auto f = [val = value]() mutable -> int {
        return val = 1314;
      };  // 需要 mutable
      auto val_f = f();
      std::cout << value << ' ' << val_f << std::endl;  // Output: 520 1314
    }

    {
      auto f = [&val = value]() -> int { return val = 1314; };  // 不需要 mutable
      auto val_f = f();
      std::cout << value << ' ' << val_f << std::endl;  // Output: 1314 1314
    }

详见 mutable 可变规范．

在 capture 中定义的变量的生命周期跟随 Lambda 表达式的接收方，在以上几个示例中为变量 𝑓 f，因为 Lambda 本身其实是一个类，capture 中的所有内容都是这个类的 `private` 成员变量，例如：

    1
    2
    3
    4
    5
    6
    7
    int main() {
      auto f = [val = 0]() mutable -> int { return ++val; };  // val 被构造和初始化

      std::cout << f() << '\n';  // Output: 1
      std::cout << f() << '\n';  // Output: 2
      std::cout << f() << '\n';  // Output: 3
    }  // val 跟随 f 被销毁

### parameters 参数列表

大多数情况下类似于函数的参数列表，例如：

    1
    2
    3
    int x[] = {5, 1, 7, 6, 1, 4, 2};
    std::sort(x, x + 7, [](int a, int b) { return (a > b); });
    for (auto i : x) std::cout << i << " ";

这将打印出 `x` 数组从大到小排序后的结果．

由于 parameters 参数列表 是可选的，如果不将参数传递给 lambda，并且其声明不包含 mutable，且没有后置返回值类型，则可以省略空括号．

使用 `auto` 声明的参数

C++14 后，若参数使用 `auto` 声明类型，那么会构造一个 泛型 Lambda 表达式．

#### 显式对象形参（C++23）

C++23 起，显式对象形参 可以在 lambda 的参数中使用．

    1
    2
    3
    4
    5
    auto nth_fibonacci = [](this auto self, unsigned n) -> unsigned {
      return n < 2 ? n : self(n - 1) + self(n - 2);
    };

    cout << nth_fibonacci(10u);

### mutable 可变规范

使得函数体可以修改通过值捕获的变量．

    1
    2
    3
    4
    5
    6
    int a = 0;
    auto by_value = [a]() mutable { ++a; };
    auto by_ref = [&a] { ++a; };

    by_value();
    by_ref();

在执行完 `by_value()` 后，`by_value` 的捕获成员 `a` 为 1，但外部的变量 `a` 依然为 0． 而在执行完 `by_ref()` 后，外部 `a` 的值变为 1．

### return-type 返回类型

用于指定 lambda 表达式的返回类型．如果省略，则返回类型将被自动推断（行为与用 `auto` 声明返回值的函数一致）．

多个 `return` 语句且推导类型不一致时，将产生编译错误．

    1
    2
    3
    4
    5
    6
    7
    8
    auto lam = [](int a, int b) -> int { return 0; };

    auto x1 = [](int i) { return i; };

    auto x2 = [](bool condition) {
      if (condition) return 1;
      return 1.0;
    };  // Error, 推导类型不一致

### 泛型 Lambda（C++14）

使用 `auto` 作为参数类型，可以构造泛型 lambda．

    1
    auto add = [](auto a, auto b) { return a + b; };

在 cpp insights 中可以观察到编译器生成的 `lambda` 类定义：

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class add_lambda {
     public:
      template <class T, class U>
      auto operator()(T a, U b) const {
        return a + b;
      }
    };

    add_lambda add{};

`add` 两个参数声明均使用了 `auto`，对应为 `add_lambda` 类的 `operator()` 函数模板的两个模板参数 `T` 和 `U`．

### Lambda 中的递归

先来看一个编译失败的例子：

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int n = 10;

    auto dfs = [&](int i) -> void {
      if (i == n)
        return;
      else
        dfs(i + 1);  // Error: a variable declared with an auto type specifier
                     // cannot appear in its own initializer
    };

我们这里尝试在捕获列表中捕获 𝑑 𝑓 𝑠 d f s，但是有一个问题，𝑑 𝑓 𝑠 d f s 的类型为 `auto`，要等待等号右边的类型推导完成后才会推导出 𝑑 𝑓 𝑠 d f s 的类型，而 Lambda 要捕获 𝑑 𝑓 𝑠 d f s 就必须要确定 𝑑 𝑓 𝑠 d f s 的类型后才能创建它的引用变量，好，这会陷入了一个套娃过程．

怎么解决这个问题呢？

1. 显式指定 𝑑 𝑓 𝑠 d f s 的类型，可以使用 `std::function` 替代．

   修改如上代码为：

        1
        2
        3
        4
        5
        6
        7
        8
        9
       10
       int n = 10;

       std::function<void(int)> dfs = [&](int i) -> void {
         if (i == n)
           return;
         else
           dfs(i + 1);  // OK
       };

       dfs(1);

   不建议使用 `std::function` 实现的递归

   `std::function` 的类型擦除通常需要分配额外内存，同时间接调用带来的寻址操作会进一步降低性能．

   在 Benchmark 测试中，使用 Clang 17 编译器，libc++ 作为标准库，`std::function` 实现比 lambda 实现的递归慢了约 2.5 倍．

   测试代码

        1
        2
        3
        4
        5
        6
        7
        8
        9
       10
       11
       12
       13
       14
       15
       16
       17
       18
       19
       20
       21
       22
       23
       24
       25
       26
       27
       28
       29
       30
       31
       32
       33
       34
       35
       36
       37
       38
       39
       40
       41
       42
       43
       44
       45
       46
       47
       48
       49
       50
       51
       52
       53
       54
       55
       #include <algorithm>
       #include <functional>
       #include <numeric>
       #include <random>

       using namespace std;

       const auto& nums = [] {
         random_device rd;
         mt19937 gen{rd()};
         array<unsigned, 32> arr{};

         std::iota(arr.begin(), arr.end(), 0u);
         ranges::shuffle(arr, gen);

         return arr;
       }();

       static void std_function_fib(benchmark::State& state) {
         std::function<int(int)> fib;

         fib = [&](int n) { return n <= 2 ? 1 : fib(n - 1) + fib(n - 2); };

         unsigned i = 0;

         for (auto _ : state) {
           auto res = fib(nums[i]);
           benchmark::DoNotOptimize(res);

           ++i;

           if (i == nums.size()) i = 0;
         }
       }

       BENCHMARK(std_function_fib);

       static void template_lambda_fib(benchmark::State& state) {
         auto n_fibonacci = [](const auto& self, int n) -> int {
           return n <= 2 ? 1 : self(self, n - 1) + self(self, n - 2);
         };

         unsigned i = 0;

         for (auto _ : state) {
           auto res = n_fibonacci(n_fibonacci, nums[i]);
           benchmark::DoNotOptimize(res);

           ++i;

           if (i == nums.size()) i = 0;
         }
       }

       BENCHMARK(template_lambda_fib);

   1. 不通过捕获的方式获取 𝑑 𝑓 𝑠 d f s，而是通过函数传参的方式．
   修改如上代码为：

        1
        2
        3
        4
        5
        6
        7
        8
        9
       10
       11
       12
       13
       14
       int n = 10;

       // 参数列表中有参数类型为 auto，则这个 Lambda 类中的 operator()
       // 函数将被定义为模板函数，模板函数可以在稍后被调用时再进行实例化
       auto dfs = [&](auto& self,
                      int i) -> void  // [&] 只会捕获用到的变量，所以不会捕获 auto dfs
       {
         if (i == n)
           return;
         else
           self(self, i + 1);  // OK
       };

       dfs(dfs, 1);

   `auto self`、`auto& self` 和 `auto&& self` 的区别：

   `auto& self` 和 `auto&& self` 理论上都只会使用 8 8 个字节（指针的大小）用作传参，不会发生其他的拷贝．具体要看编译器对 Lambda 的实现方式和对应的优化． 而使用 `auto self` 会发生对象拷贝，拷贝的大小取决于捕获列表中的元素，因为它们都是这个 Lambda 类中的私有成员变量．

   1. 可以通过手动展开 Lambda 类，或使用类似写法，这样可以直接声明 𝑑 𝑓 𝑠 d f s 的类型．
   修改如上代码为：

        1
        2
        3
        4
        5
        6
        7
        8
        9
       10
       11
       12
       13
       14
       15
       16
       17
       18
       int n = 10;

       class Lambda_1 {
        public:
         auto operator()(int i) const -> void {
           if (i == n)
             return;
           else
             (*this)(i + 1);  // OK
         }

         explicit Lambda_1(int& __n) : n(__n) {}

        private:
         int& n;
       } dfs(n);

       dfs(1);

   1. 如果 lambda 没有捕获任何变量，我们也可以利用函数指针．

   如果 lambda 没有捕获任何变量，那么它可以隐式转换为函数指针．同时 lambda 此时也可以声明为 `static`，函数指针类型也可以声明为 `static`．如此依赖，lambda 可以不需要捕获就能访问函数指针，从而实现递归．

   示例

        1
        2
        3
        4
        5
        6
        7
        8
        9
       10
       11
       12
       13
       14
       static unsigned (*fptr)(unsigned);

       static const auto lambda = [](const unsigned a) {
         return a < 2 ? a : (*fptr)(a - 2) + (*fptr)(a - 1);
       };

       static auto init = [] {
         fptr = +lambda;
         // Or
         // fptr = static_cast<unsigned (*)(unsigned)>(lambda);
         return 0;
       }();

       cout << lambda(10);

### Lambda 表达式的应用

#### 作为标准库算法的 Predicate（谓词）

从大到小排序：

    1
    2
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

使用 std::find_if 查找第一个大于 3 的元素：

    1
    2
    std::vector<int> v = {1, 2, 3, 4, 5};
    auto it = std::find_if(v.begin(), v.end(), [](int a) { return a > 3; });

#### 控制中间变量的生命周期

在算法竞赛中，我们会遇到这样的场景：一个变量的初始化需要使用之前声明的变量，其初始化过程又生成占用空间较大的中间变量．

我们希望能尽快析构这些中间变量，以降低内存消耗．此时，我们可以使用 lambda 来控制这些中间变量的生命周期．

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    void solution(const vector<int>& input) {
      int b = [&] {
        vector<int> large_objects(input.size());
        int c = 0;

        for (int i = 0; i < large_objects.size(); ++i)
          large_objects[i] = i + input[i];

        for (int i = 0; i < input.size(); ++i) c += large_objects[input[i]];

        return c;
      }();

      // ...
    }

相较于使用块作用域，lambda 可以允许我们使用返回值，使得代码更加简洁；相较于函数，我们不需要额外起名和声明被捕获的各种参数，使得代码更加紧凑．

## 参考文献

- cppreference-lambda
- Stackoverflow: Overhead with std::function

> 本页面最近更新：2026/2/26 03:56:39，更新历史
> 发现错误？想一起完善？ 在 GitHub 上编辑此页！
> 本页面贡献者：Tiphereth-A, c0nstexpr, c-forrest, CCXXXI, CoderOJ, Great-designer, hly1204, huanhuanonly, Persdre, shuzhouliu, ZnPdCo, zyzzyh
> 本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供，附加条款亦可能应用