下面分成"常见面试题"和"常见坑"两部分。代码没有实测过,涉及 sizeof 的数值以 GCC/libstdc++(x86_64)为准,属于实现细节。

一、常见面试题

1. lambda 的本质是什么

lambda 表达式会生成一个匿名类(闭包类型)的对象

  • 捕获的变量成为这个类的数据成员。
  • 函数体成为这个类的 operator(),而且默认是 const 成员函数
int k = 10;
auto f = [k](int x) { return x + k; };

// 编译器生成的类大致相当于:
class __lambda_1 {
    int k;                                   // 值捕获 → 成员
public:
    __lambda_1(int k) : k(k) {}
    int operator()(int x) const { return x + k; }   // 默认 const
};
auto f = __lambda_1{k};

每个 lambda 表达式都有自己独一无二的类型。即使两个 lambda 写法完全相同,它们的类型也不同。

2. 捕获方式有哪些

写法 含义
[] 不捕获任何变量
[x] / [&x] 按值 / 按引用捕获 x
[=] / [&] 用到的变量全部按值 / 按引用捕获(只捕获函数体里实际用到的变量,不是作用域里的所有变量)
[=, &x] / [&, x] 默认方式加例外
[this] 捕获 this 指针,成员通过指针访问
[*this](C++17) 拷贝一份整个对象
[p = std::move(up)](C++14) 初始化捕获,可以移动、改名,或者捕获一个表达式的值

3. mutable 有什么用

它去掉 operator()const,让函数体可以修改按值捕获的那份副本。外面的原变量不受影响。

int n = 0;
auto f = [n]() mutable { return ++n; };
f(); f();          // 返回 1、2
// n 仍然是 0

4. sizeof(lambda) 是多少

  • 没有捕获:1,因为空类的大小至少是 1。
  • 按值捕获:各成员大小之和,再加上对齐填充。例如 [a = 'c', b = 1.0] 的大小是 16。
  • 按引用捕获:由实现决定,通常每个引用占一个指针的大小。

5. lambda 能转换成函数指针吗

只有无捕获的 lambda 可以,而且是隐式转换。这是把 lambda 传给 C 接口(qsortpthread_createatexit)的方法。

void (*fp)(int) = [](int x) { /* ... */ };   // OK
auto p = +[](int x) { return x * 2; };      // 一元 + 强制转成函数指针

有捕获的 lambda 不能转换,因为函数指针没有地方存放捕获的状态。

6. lambda 和 std::function 有什么区别

lambda(auto 或模板参数接收) std::function<R(Args...)>
类型 每个 lambda 都是独立的具体类型 类型擦除,签名相同的可调用对象都能装进去
调用 直接调用,可以内联 间接调用,通常无法内联
存储 在栈上,大小就是捕获的内容 对象本身 32 字节(libstdc++);捕获的东西放不进小缓冲区时要堆分配
要求 可调用对象必须能拷贝(C++23 有 std::move_only_function

原则:只需要把 lambda 传进函数时,用模板参数或 auto 参数接收;需要存储不同类型的回调时,才用 std::function

7. 泛型 lambda 和模板 lambda

auto g = [](auto x, auto y) { return x + y; };      // C++14:operator() 是函数模板
auto h = []<class T>(std::vector<T>& v) { /*...*/ }; // C++20:能直接写出 T

8. lambda 怎么递归

lambda 在自己的函数体里无法引用自己(它没有名字)。有三种办法:

// ① std::function:简单,但有类型擦除的开销,而且存在悬空风险(见坑 9)
std::function<int(int)> fib = [&](int n) { return n < 2 ? n : fib(n-1) + fib(n-2); };

// ② 把自己当参数传进去(C++14)
auto fib = [](auto self, int n) -> int { return n < 2 ? n : self(self, n-1) + self(self, n-2); };
fib(fib, 10);

// ③ C++23 deducing this:最干净
auto fib = [](this auto self, int n) -> int { return n < 2 ? n : self(n-1) + self(n-2); };

9. 各标准版本给 lambda 加了什么

版本 新增
C++11 lambda 本身
C++14 泛型 lambda(auto 参数)、初始化捕获
C++17 constexpr lambda(满足条件时自动成为 constexpr)、[*this]
C++20 模板 lambda、consteval、无状态 lambda 可以默认构造和赋值、lambda 可以出现在 decltype 里、能捕获结构化绑定、[=] 隐式捕获 this 被弃用
C++23 deducing this、static operator()[]() static {})、可以写属性

10. 立即调用的 lambda 有什么用

用来对复杂的初始化逻辑做 const 初始化:

const auto config = [&] {
    Config c;
    if (debug) c.level = 3;
    // ...
    return c;
}();   // 注意末尾的 ()

二、常见坑

坑 1:引用捕获导致悬空(最常考)

std::function<int()> make() {
    int x = 42;
    return [&] { return x; };   // x 在函数返回后销毁,引用悬空,调用是 UB
}

在异步回调、线程、存起来的回调里,都不要用 [&]

坑 2:[=] 在成员函数里捕获的是 this,不是成员的副本

struct Widget {
    int value = 0;
    auto get_callback() {
        return [=] { return value; };   // 实际是 this->value,捕获的是 this 指针
    }
};
auto cb = Widget{}.get_callback();       // 临时 Widget 已经销毁
cb();                                    // UB

写成 [=] 看起来像按值捕获,实际上成员仍按引用访问。修法:

  • [*this] 拷贝整个对象。
  • [v = value] 只拷贝需要的成员。
  • 需要让对象活到回调执行时,用 [self = shared_from_this()]

C++20 已经弃用 [=] 隐式捕获 this,要求显式写出 [=, this]

坑 3:值捕获发生在创建时,不是调用时

int x = 1;
auto f = [x] { return x; };
x = 2;
f();            // 返回 1

坑 4:mutable lambda 被拷贝后,状态各自独立

auto counter = [n = 0]() mutable { return ++n; };
std::function<int()> f1 = counter;   // 拷贝了一份
f1(); f1();                          // 2
counter();                           // 1 —— 原对象的 n 没有变

标准算法按值接收函数对象(std::for_eachstd::generate),调用方手里那个 lambda 的状态不会被更新。

坑 5:静态变量和全局变量不会被捕获

static int s = 0;
auto f = [=] { return ++s; };   // 能编译:s 根本没被捕获,直接访问的是全局那一个

[=] 看起来像"拷贝了一份",实际上改的是全局的变量。

坑 6:只能移动的捕获,让 lambda 放不进 std::function

auto up = std::make_unique<int>(1);
auto f = [p = std::move(up)] { return *p; };   // f 只能移动,不能拷贝
std::function<int()> g = std::move(f);         // 编译错误:std::function 要求可拷贝

在 C++23 里改用 std::move_only_function。在那之前只能改用 shared_ptr,或者自己写一个包装。

坑 7:想把捕获的成员移出去,必须加 mutable

auto f = [s = std::string("hi")]() { return std::move(s); };
// operator() 是 const,std::move(s) 得到的是 const std::string&&,
// 匹配到的是拷贝构造 —— 能编译,但悄悄变成了拷贝
auto g = [s = std::string("hi")]() mutable { return std::move(s); };   // 真正移动

坑 8:在循环里按引用捕获循环变量

std::vector<std::thread> ts;
for (int i = 0; i < 4; ++i)
    ts.emplace_back([&] { use(i); });   // 线程运行时 i 可能已经变了,甚至已经离开作用域

改成 [i] 按值捕获。

坑 9:用 std::function 递归时,按引用捕获了自己

std::function<int(int)> make_fib() {
    std::function<int(int)> fib = [&fib](int n) { return n < 2 ? n : fib(n-1) + fib(n-2); };
    return fib;   // 返回的是一份拷贝,里面捕获的仍是局部变量 fib 的引用,悬空
}

坑 10:两个分支返回不同的 lambda

auto pick(bool b) {
    if (b) return [](int x) { return x; };
    else   return [](int x) { return -x; };   // 编译错误:两个 return 推导出的类型不一样
}

每个 lambda 的类型都不同。无捕获的可以用 + 统一转成函数指针;有捕获的只能返回 std::function

坑 11:std::function 带来的性能损失

在热点路径上,用 std::function 接收 lambda 会失去内联,还可能多一次堆分配。接收参数时用模板参数,或者 C++20 的 std::invocable auto&& f

坑 12:lambda 协程的捕获悬空

lambda 本身是协程时,协程帧里保存的是指向闭包对象的指针,而不是捕获变量的副本。闭包对象一旦先销毁(比如它是个临时对象),协程恢复执行后再访问捕获的变量就是悬空的。C++ 核心准则 CP.51 的建议是:不要让协程 lambda 带捕获,改用参数传入,因为参数会被拷贝进协程帧。

三、面试高频题速答

  1. lambda 是什么? 编译器生成的匿名类对象:捕获的变量成为成员,函数体成为 const operator()
  2. 什么时候用 mutable 需要修改按值捕获的副本时。
  3. 能转成函数指针吗? 只有无捕获的可以。
  4. [=] 在成员函数里有什么问题? 捕获的是 this,对象销毁后回调就悬空。
  5. lambda 和 std::function 怎么选? 传参时用模板,存储时才用 std::function
  6. 怎么捕获 unique_ptr 用初始化捕获 [p = std::move(p)],代价是 lambda 只能移动。
  7. lambda 怎么递归? C++23 用 deducing this;之前把自己作为参数传入,或者用 std::function