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 的核心逻辑:

mutable Lambda 核心逻辑 Lambda 捕获变量 值捕获(副本) vs 引用捕获(别名) 值捕获:默认 operator() const 引用捕获:可直接修改 加 mutable → operator() 非 const 可修改副本,但不影响原始变量 直接修改原始变量 mutable 只对值捕获有意义,引用捕获不需要 mutable

9.8 总结

mutable 是个小特性,但用好了能让代码更优雅。记住三点:

  • mutable 修改的是副本,不是原始变量
  • 引用捕获不需要 mutable,加了反而多余
  • mutable Lambda 内部状态不是线程安全的,多线程要小心

我个人觉得,mutable 最适合的场景是「内部状态机」——你需要在 Lambda 内部维护一些状态,但又不想暴露给外部。比如计数器、缓存、累加器等。

嗯,关于 mutable 就聊这么多。记住:它给你修改副本的权利,但别指望能改变外面的世界。

小技巧:如果你发现自己在 Lambda 里频繁修改捕获的变量,不妨想想能不能用引用捕获代替。引用捕获更直接,也更容易理解。