跳转至

lambda 表达式

标准 难度 状态 分级 证据 审阅 最后校验
C++11 3/5 final A 3 条(manual 3) 单审(zyuhao7) 2026-10-27

编译器支持:GCC 4.5+ · Clang 3.1+ · MSVC 16.0+。版本查询:cppreference 编译器支持。

主张与证据

主张 支持证据
用捕获 this 的 lambda 把成员调用包装成延迟执行的刷新回调 ev-spdlog-lambda-registry-001
用谓词 lambda 作为条件变量的等待条件 ev-spdlog-lambda-mpmc-wait-002
用按值捕获 key 的 lambda 构造生命周期更长的超时回调 ev-drogon-lambda-cache-timeout-003

本站证据页:ev-drogon-lambda-cache-timeout-003 · ev-spdlog-lambda-mpmc-wait-002 · ev-spdlog-lambda-registry-001

解决什么问题

C++11 之前,要把“一段行为”传给算法或回调接口,只有几种选择:写一个具名函数,但无法携带上下文;手写仿函数类,样板代码多;用 std::bind 拼装,语法晦涩且类型推导差;或者用函数指针加 void* 上下文,丢掉类型安全。回调逻辑往往只有一两行,却要在远处定义一个完整类型。

lambda 表达式在调用点直接写出匿名函数对象,并显式声明要捕获的外部变量。编译器为每个 lambda 生成唯一的闭包类型,捕获的变量成为该类型的成员。标准提案为 N2927。

基础用法

方括号是捕获列表,圆括号是参数列表,花括号是函数体。没有捕获的 lambda 可以转换为函数指针。

#include <algorithm>
#include <iostream>
#include <vector>

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

  int threshold = 2;
  auto above = [threshold](int value) { return value > threshold; };

  long total = 0;
  std::for_each(values.begin(), values.end(),
                [&total](int value) { total += value; });

  const auto count = std::count_if(values.begin(), values.end(), above);
  std::cout << count << ' ' << total << '\n';
}

输出 3 14:排序按降序进行,above 按值保存了 threshold,total 通过引用被累加。完整版本见 examples/cpp11/lambda.cpp。

进阶用法

捕获列表决定闭包如何持有外部状态。[=] 按值捕获用到的变量,[&] 按引用捕获,[x, &y] 可以逐个指定。按值捕获的变量默认在 lambda 体内是 const,要修改副本需加 mutable。捕获发生在 lambda 创建时,之后外部变量的变化不会反映到按值捕获的副本上。

C++14 增加初始化捕获,可以写 [n = 0] 或把 std::move 的结果放进闭包;同时允许 auto 参数形成泛型 lambda。C++17 允许 constexpr lambda,C++20 允许模板参数列表和无捕获 lambda 的默认构造。需要递归时,通常先把 lambda 存入 std::function,或使用 Y 组合子式的显式自引用参数。

捕获引用意味着闭包的生命周期不能超过被引用对象。把 lambda 交给异步任务、线程或事件循环时,必须确认捕获对象的存活时间;跨线程场景通常改为按值捕获,或捕获 std::shared_ptr 延长生命周期。

真实项目证据

项目 位置 用法 为什么典型
spdlog include/spdlog/details/registry.h#L67-L72 捕获 this 生成周期性刷新回调 一行逻辑无需具名仿函数,捕获明确依赖对象
spdlog include/spdlog/details/mpmc_blocking_q.h#L30-L38 用 lambda 作为 wait 的谓词 判断逻辑与等待调用就地绑定,作用域清晰
drogon lib/inc/drogon/CacheMap.h#L528-L541 按值捕获 key 构造超时回调并存入函数对象 回调生命周期长于当前调用,捕获方式关键

三条证据均为 manual 级:作者复核,尚未引入第二名审核者。三处都在生产路径上,覆盖延迟任务、条件等待与异步回调三种用途。

陷阱与边界

悬空引用。 [&] 捕获的变量随作用域结束而失效;把 lambda 存起来稍后调用时,引用捕获几乎必然出错。异步接口要检查回调的调用时机。

按值捕获不等于复制所有变量。 只有 lambda 体内实际用到的变量才会被捕获;[=] 也不是“复制整个作用域”。

mutable 改变的是副本。 加 mutable 后修改的是闭包自己的成员,外部变量不受影响;要修改外部变量必须按引用捕获。

每个 lambda 类型唯一。 两个文本相同的 lambda 仍是不同类型,不能互相赋值;需要统一类型时用 std::function 或函数指针。

捕获 this 不延长对象寿命。 在成员函数里写 [this] 只是保存指针;对象销毁后调用回调是未定义行为。C++20 起隐式 [=] 捕获 this 被弃用,应显式写出。

避免在性能关键路径滥用 std::function。 它可能分配堆内存并引入间接调用;泛型算法直接接受 lambda 通常可以内联。

库联动

  • <algorithm>:sort、find_if、count_if、for_each 等接受谓词或变换函数,lambda 是最常用的实参。
  • std::function:需要类型擦除、存储异构回调时把 lambda 装进去,代价是一次可能的分配和间接调用。
  • 并发设施:std::thread、std::async、条件变量谓词都用 lambda 表达线程入口与等待条件。
  • spdlog / drogon:日志库用 lambda 注册周期刷新,Web 框架用 lambda 封装缓存超时回调。

横向对比

方案 携带上下文 类型安全 可内联 主要限制
lambda 是 是 通常可 每个闭包类型唯一
函数指针 否 是 视情况 需要 void* 传递上下文
手写仿函数 是 是 可 样板代码多,远离调用点
std::bind 是 较弱 较难 占位符语法晦涩,易产生悬空引用
std::function 是 是 否 类型擦除带来分配与间接调用

跨标准演进:C++11 通过 N2927 引入 lambda;C++14 增加泛型 lambda 与初始化捕获;C++17 允许 constexpr lambda;C++20 增加模板参数列表、无捕获 lambda 的默认构造,并弃用隐式捕获 this。