9. 可变 Lambda:mutable 关键字,修改值捕获的副本
聊到 Lambda 表达式,有个问题经常被问到:「Lambda 里能不能修改捕获的变量?」
答案是:默认不能,但加了 mutable 就可以。
嗯,这里要注意——mutable 修改的是副本,不是原始变量。 这一点特别容易搞混。我见过不少同事,一开始以为 mutable 能改外面的变量,结果调试了半天才发现改的是拷贝。
9.1 默认行为:Lambda 是「只读」的
先看一个最简单的例子:
int x = 10;
auto lambda = [x]() {
// x = 20; // 编译错误!
};
为什么会报错?
因为 Lambda 默认把值捕获的变量视为 const 引用。说白了,编译器生成的 operator() 是 const 的:
class Lambda {
int x; // 捕获的副本
public:
void operator()() const {
// x = 20; // const 成员函数不能修改成员变量
}
};
我个人习惯把 Lambda 想象成一个「迷你对象」。你捕获的每个变量,都是这个对象的成员变量。而默认情况下,operator() 是 const 的,所以你不能修改它们。
9.2 mutable 关键字:打开修改权限
加上 mutable 之后,operator() 就不再是 const 了:
int x = 10;
auto lambda = [x]() mutable {
x = 20; // 可以修改了
std::cout << x << std::endl; // 输出 20
};
lambda();
std::cout << x << std::endl; // 输出 10(原始变量没变)
你看,mutable 修改的是 Lambda 内部的副本,外面的 x 纹丝不动。
我在项目中遇到过这样一个场景:写一个计数器,每次调用 Lambda 就自增一次。当时我第一反应是用引用捕获,但后来发现用 mutable 更安全——因为我不想让外部变量被意外修改。
int counter = 0;
auto increment = [counter]() mutable {
counter++;
return counter;
};
std::cout << increment() << std::endl; // 1
std::cout << increment() << std::endl; // 2
std::cout << counter << std::endl; // 0(外部变量没变)
这个例子很典型。你想想看,如果我用引用捕获,counter 就会被改掉,可能引发意想不到的副作用。而 mutable 保证了「内部自嗨,外部不受影响」。
9.3 mutable 与引用捕获
这里有个容易混淆的点:引用捕获不需要 mutable。
int x = 10;
auto lambda = [&x]() {
x = 20; // 直接修改原始变量,不需要 mutable
};
为什么?因为引用捕获的是原始变量的别名,你修改的是原始变量本身,不是副本。所以 mutable 只对值捕获有意义。
我曾经在代码评审中看到有人这样写:
int x = 10;
auto lambda = [&x]() mutable {
x = 20;
};
嗯,mutable 在这里是多余的。虽然不会报错,但容易让人误解——以为修改的是副本,其实改的是原始变量。我个人建议:引用捕获就别加 mutable,保持代码清晰。
9.4 mutable 的实际应用场景
除了计数器,mutable 还有几个常见用途:
- 生成唯一 ID:每次调用 Lambda 返回一个递增的 ID
- 缓存计算结果:第一次计算后缓存结果,后续直接返回
- 状态机:在 Lambda 内部维护状态,比如记录调用次数
看一个缓存例子:
auto create_cacher = []() {
int cache = 0;
bool cached = false;
return [cache, cached]() mutable {
if (!cached) {
cache = expensive_computation();
cached = true;
}
return cache;
};
};
这个 Lambda 内部维护了 cache 和 cached 两个状态。第一次调用时执行计算,后续直接返回缓存值。外部完全感知不到这些内部状态。
核心要点:mutable 让 Lambda 内部的状态「活」了起来,但不会污染外部作用域。
9.5 避坑指南:mutable 的陷阱
我曾经在项目里踩过一个坑:
std::vector<std::function<void()>> tasks;
for (int i = 0; i < 5; ++i) {
tasks.push_back([i]() mutable {
std::cout << i << " ";
i++; // 修改副本
});
}
for (auto& task : tasks) {
task(); // 输出:0 1 2 3 4
}
看起来没问题对吧?但如果你把 Lambda 存起来多次调用:
auto task = [i = 0]() mutable {
std::cout << i << " ";
i++;
};
task(); // 0
task(); // 1
task(); // 2
这里 i 是 Lambda 内部的持久状态。每次调用都会修改它。如果你不小心把这样的 Lambda 传给多个线程,就会产生数据竞争。
警告:mutable Lambda 内部的状态不是线程安全的。如果要在多线程中使用,需要自己加锁或使用原子变量。
9.6 mutable 与 const Lambda
你可能会问:Lambda 本身能不能是 const 的?
可以。用 const 修饰 Lambda 变量:
int x = 10;
const auto lambda = [x]() mutable {
x = 20; // 编译错误!const 对象不能调用非 const 成员函数
};
这里有个有趣的细节:const Lambda + mutable 是矛盾的。因为 mutable 让 operator() 变成非 const,而 const 对象只能调用 const 成员函数。所以两者不能同时使用。
我个人习惯:能用 const 就用 const,除非你明确需要修改内部状态。这样代码更安全,意图也更清晰。
9.7 知识体系图
下面这张图总结了 mutable Lambda 的核心逻辑:
9.8 总结
mutable 是个小特性,但用好了能让代码更优雅。记住三点:
- mutable 修改的是副本,不是原始变量
- 引用捕获不需要 mutable,加了反而多余
- mutable Lambda 内部状态不是线程安全的,多线程要小心
我个人觉得,mutable 最适合的场景是「内部状态机」——你需要在 Lambda 内部维护一些状态,但又不想暴露给外部。比如计数器、缓存、累加器等。
嗯,关于 mutable 就聊这么多。记住:它给你修改副本的权利,但别指望能改变外面的世界。
小技巧:如果你发现自己在 Lambda 里频繁修改捕获的变量,不妨想想能不能用引用捕获代替。引用捕获更直接,也更容易理解。