lambda表达式
下面分成"常见面试题"和"常见坑"两部分。代码没有实测过,涉及 sizeof 的数值以 GCC/libstdc++(x86_64)为准,属于实现细节。
一、常见面试题
1. lambda 的本质是什么
lambda 表达式会生成一个匿名类(闭包类型)的对象:
- 捕获的变量成为这个类的数据成员。
- 函数体成为这个类的
operator(),而且默认是const成员函数。
int k = 10;
auto f = ;
// 编译器生成的类大致相当于:
;
auto f = __lambda_1;
每个 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 = mutable ;
; ; // 返回 1、2
// n 仍然是 0
4. sizeof(lambda) 是多少
- 没有捕获:1,因为空类的大小至少是 1。
- 按值捕获:各成员大小之和,再加上对齐填充。例如
[a = 'c', b = 1.0]的大小是 16。 - 按引用捕获:由实现决定,通常每个引用占一个指针的大小。
5. lambda 能转换成函数指针吗
只有无捕获的 lambda 可以,而且是隐式转换。这是把 lambda 传给 C 接口(qsort、pthread_create、atexit)的方法。
void = ; // OK
auto p = + ; // 一元 + 强制转成函数指针
有捕获的 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 = ; // C++14:operator() 是函数模板
auto h = <class T> ; // C++20:能直接写出 T
8. lambda 怎么递归
lambda 在自己的函数体里无法引用自己(它没有名字)。有三种办法:
// ① std::function:简单,但有类型擦除的开销,而且存在悬空风险(见坑 9)
std::function<int> fib = ;
// ② 把自己当参数传进去(C++14)
auto fib = -> int ;
;
// ③ C++23 deducing this:最干净
auto fib = -> int ;
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 = ; // 注意末尾的 ()
二、常见坑
坑 1:引用捕获导致悬空(最常考)
std::function<int>
在异步回调、线程、存起来的回调里,都不要用 [&]。
坑 2:[=] 在成员函数里捕获的是 this,不是成员的副本
;
auto cb = Widget.; // 临时 Widget 已经销毁
; // UB
写成 [=] 看起来像按值捕获,实际上成员仍按引用访问。修法:
- 用
[*this]拷贝整个对象。 - 用
[v = value]只拷贝需要的成员。 - 需要让对象活到回调执行时,用
[self = shared_from_this()]。
C++20 已经弃用 [=] 隐式捕获 this,要求显式写出 [=, this]。
坑 3:值捕获发生在创建时,不是调用时
int x = 1;
auto f = ;
x = 2;
; // 返回 1
坑 4:mutable lambda 被拷贝后,状态各自独立
auto counter = mutable ;
std::function<int> f1 = counter; // 拷贝了一份
; ; // 2
; // 1 —— 原对象的 n 没有变
标准算法按值接收函数对象(std::for_each、std::generate),调用方手里那个 lambda 的状态不会被更新。
坑 5:静态变量和全局变量不会被捕获
static int s = 0;
auto f = ; // 能编译:s 根本没被捕获,直接访问的是全局那一个
[=] 看起来像"拷贝了一份",实际上改的是全局的变量。
坑 6:只能移动的捕获,让 lambda 放不进 std::function
auto up = std::make_unique<int>;
auto f = ; // f 只能移动,不能拷贝
std::function<int> g = ; // 编译错误:std::function 要求可拷贝
在 C++23 里改用 std::move_only_function。在那之前只能改用 shared_ptr,或者自己写一个包装。
坑 7:想把捕获的成员移出去,必须加 mutable
auto f = ;
// operator() 是 const,std::move(s) 得到的是 const std::string&&,
// 匹配到的是拷贝构造 —— 能编译,但悄悄变成了拷贝
auto g = mutable ; // 真正移动
坑 8:在循环里按引用捕获循环变量
std::vector<std::thread> ts;
for
ts.; // 线程运行时 i 可能已经变了,甚至已经离开作用域
改成 [i] 按值捕获。
坑 9:用 std::function 递归时,按引用捕获了自己
std::function<int>
坑 10:两个分支返回不同的 lambda
auto
每个 lambda 的类型都不同。无捕获的可以用 + 统一转成函数指针;有捕获的只能返回 std::function。
坑 11:std::function 带来的性能损失
在热点路径上,用 std::function 接收 lambda 会失去内联,还可能多一次堆分配。接收参数时用模板参数,或者 C++20 的 std::invocable auto&& f。
坑 12:lambda 协程的捕获悬空
lambda 本身是协程时,协程帧里保存的是指向闭包对象的指针,而不是捕获变量的副本。闭包对象一旦先销毁(比如它是个临时对象),协程恢复执行后再访问捕获的变量就是悬空的。C++ 核心准则 CP.51 的建议是:不要让协程 lambda 带捕获,改用参数传入,因为参数会被拷贝进协程帧。
三、面试高频题速答
- lambda 是什么? 编译器生成的匿名类对象:捕获的变量成为成员,函数体成为
const operator()。 - 什么时候用
mutable? 需要修改按值捕获的副本时。 - 能转成函数指针吗? 只有无捕获的可以。
[=]在成员函数里有什么问题? 捕获的是this,对象销毁后回调就悬空。- lambda 和
std::function怎么选? 传参时用模板,存储时才用std::function。 - 怎么捕获
unique_ptr? 用初始化捕获[p = std::move(p)],代价是 lambda 只能移动。 - lambda 怎么递归? C++23 用 deducing this;之前把自己作为参数传入,或者用
std::function。
暂无评论,欢迎留下第一条评论。