CC++ & Algorithm

Lambda捕获列表与高级用法

极难2
语言版本:通用
概述:通过捕获列表,Lambda可以“借用”外部的变量,就像邮差记住地址一样;高级用法包括引用捕获、初始化捕获,让Lambda更灵活。

小纸条如何记住外面的世界?——C++ Lambda 捕获列表完全指南

想象一下:你正在写一个“自动计算零花钱”的小程序。你有一个 for_each 循环,遍历每月零花钱的列表,每次遇到一个数值就累加到外部变量 total 上。但是,Lambda 默认就像是一个“只带自己文具盒”的学生,它看不见桌子外面的东西。如果想让它使用外面的变量,就得通过 捕获列表 给它递一张“借用清单”,告诉它:“这些变量你可以借用,并且规定好是按原样抄一份(值捕获),还是直接看原版(引用捕获)。”

今天我们就来彻底搞懂这张“清单”怎么写、有哪些注意事项,以及如何用它写出简洁又强大的代码。

捕获列表:一张完整的“借用清单”

捕获列表放在 Lambda 表达式的 [] 中,可以混合多种方式。每个方式都有特定的用途,像生活中不同颜色的便签。

写法含义生活类比
[x]按值捕获变量 x(拷贝一份到 Lambda 内部)你把书借给同学,同学自己抄了一份笔记,改动不影响你的原书
[&x]按引用捕获变量 x(直接操作原变量)同学直接在原书上写批注,改动会体现在你的书上
[=]按值捕获所有用到的外部局部变量把所有需要的东西都复印一份带进考场
[&]按引用捕获所有用到的外部局部变量把所有人都叫进考场,直接在原卷上改答案
[=, &x]默认按值捕获,但 x 按引用除了 x 是直接操作原变量,其他都复印
[this]捕获当前对象的指针(类成员函数内)走进考场前,带上学生证(代表你本人)
[x = std::move(y)]初始化捕获:用 y 移动构造一个新变量 x把一支笔送给别人,别人得到笔,你自己没笔了

注意:如果没有捕获列表(空 []),Lambda 只能使用自己的参数和全局变量,不能碰任何外部局部变量。

为什么需要捕获?从两个真实场景入手

场景1:统计超过某个分数的同学人数

你有一列考试成绩,要统计超过 60 分的同学数量。外部变量 limit = 60count = 0 都需要被 Lambda 使用。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> scores = {45, 78, 90, 32, 88, 59};
    int limit = 60;    // 及格线
    int count = 0;     // 计数器
    
    for_each(scores.begin(), scores.end(), 
        [limit, &count](int s) {  // limit 值捕获(只读),count 引用捕获(可修改)
            if (s > limit) ++count;
        });
    
    cout << "及格人数: " << count << endl;  // 输出 3 (78,90,88)
    return 0;
}

这里 limit 只需要读取,所以用值捕获(拷贝一份,安全且不影响原变量);count 需要修改,所以用引用捕获。

场景2:在成员函数内打印带前缀的信息

你写了一个游戏,每个角色有自己的 namehp。想在函数内过滤出血量大于 0 的角色,并打印。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Character {
    string name;
    int hp;
public:
    Character(const string& n, int h) : name(n), hp(h) {}
    
    void showAlive(const vector<Character>& team) {
        // Lambda 需要访问当前对象的 name 前缀?实际上这里需要访问成员吗?
        // 更常见的场景:在类成员函数内调用 Lambda,需要访问其他成员
        for_each(team.begin(), team.end(), 
            [this](const Character& c) {  // 捕获 this 指针
                if (c.hp > 0) {
                    cout << "成员 " << c.name << " 还活着!" << endl;
                }
            });
    }
};

如果不捕获 this,Lambda 内就无法访问 Character 的私有成员。

常见错误与陷阱

1. 引用捕获导致悬空引用

这是最危险的错误。如果你把 Lambda 存到某个地方,而它引用的局部变量已经销毁,那么 Lambda 调用时访问的就是非法内存。

function<int()> badLambda() {
    int x = 42;
    return [&x]() { return x; };  // 危险!引用捕获局部变量,函数返回后 x 销毁
}

auto f = badLambda();
cout << f();  // 未定义行为,可能输出垃圾值

解决方法:如果要返回 Lambda,必须使用值捕获,或者用初始化捕获移动。

2. 按值捕获默认不能修改

初学者常犯的错误是认为值捕获后可以随意修改:

int a = 10;
auto lam = [a]() { a++; };  // 编译错误:a 是 const

即使你想修改副本,也必须加上 mutable 关键字。但 mutable 只修改副本,不影响外部。

3. 值捕获大对象导致性能问题

如果你值捕获一个大型对象(如 vector<int> 有 10 万个元素),Lambda 会拷贝一份,开销极大。此时应该用引用捕获或初始化捕获(移动)。

vector<int> bigData(100000, 1);
// 糟糕:拷贝了整个 bigData
auto lam1 = [bigData]() { cout << bigData.size(); };

// 正确:引用捕获,但注意生命周期
auto lam2 = [&bigData]() { cout << bigData.size(); };

// 更好的移动捕获(C++14)
auto lam3 = [data = move(bigData)]() { cout << data.size(); };
// 移动后 bigData 为空,但 lam3 拥有数据

4. 递归 Lambda 无法直接调用自身

因为 Lambda 是匿名类型,在它的体内无法用名字调用自己。需要借助 std::function 或者用 Y 组合子(一般用 std::function 更简单)。

// 错误:lambda 体中无法使用自身的名字
auto factorial = [](int n) { return n <= 1 ? 1 : n * factorial(n-1); };

// 正确:用 std::function 包装
function<int(int)> factorial = [&](int n) -> int {
    return n <= 1 ? 1 : n * factorial(n-1);
};

完整可运行示例:模拟计算器

下面这个程序模拟一个简单的计算器,用 Lambda 捕获外部操作次数,并支持多种运算。

#include <iostream>
#include <functional>   // for std::function
#include <vector>
using namespace std;

int main() {
    // 创建一个计算器:记录操作次数
    int operationCount = 0;   // 操作次数计数器
    
    // 加法函数 —— 捕获计数器的引用,每调用一次就加1
    function<int(int,int)> add = [&operationCount](int a, int b) -> int {
        operationCount++;
        return a + b;
    };
    
    // 乘法 —— 同样捕获计数器
    function<int(int,int)> multiply = [&operationCount](int a, int b) -> int {
        operationCount++;
        return a * b;
    };
    
    // 使用 Lambda 作为回调:对列表中的每对数进行操作
    vector<pair<int,int>> nums = {{3,4}, {5,2}, {6,7}};
    
    cout << "加法结果: ";
    for (auto& p : nums) {
        cout << add(p.first, p.second) << " ";   // 调用 add,count 递增
    }
    cout << endl;
    
    cout << "乘法结果: ";
    for (auto& p : nums) {
        cout << multiply(p.first, p.second) << " ";
    }
    cout << endl;
    
    cout << "总操作次数: " << operationCount << endl;  // 输出 6
    
    // 演示初始化捕获:创建一个带有定制偏移量的加法器
    int base = 100;
    auto customAdder = [baseValue = base * 2](int x) {  // 初始化一个新变量 baseValue
        return x + baseValue;
    };
    cout << "baseValue 初始化为 base*2 = " << base * 2 << ",调用 customAdder(5) = " 
         << customAdder(5) << endl;  // 输出 205
    
    return 0;
}

Python 版对照

Python 没有捕获列表语法,但闭包(嵌套函数)天然支持捕获外部变量。需要注意 nonlocal 关键字来修改外层变量。

def main():
    # 对应 C++ 的引用捕获:使用列表模拟可变对象
    operation_count = [0]   # 用列表包装,方便在内部修改
    
    def add(a, b):
        operation_count[0] += 1
        return a + b
    
    def multiply(a, b):
        operation_count[0] += 1
        return a * b
    
    nums = [(3,4), (5,2), (6,7)]
    print("加法结果:", [add(a,b) for a,b in nums])
    print("乘法结果:", [multiply(a,b) for a,b in nums])
    print("总操作次数:", operation_count[0])
    
    # 初始化捕获类似:创建新的局部变量
    base = 100
    custom_adder = lambda x: x + base * 2   # 直接使用外部变量,Python 闭包捕获的是引用
    # 但注意:Python 的 lambda 捕获的是变量对象本身,如果 base 是 immutable 类型,
    # 后续改 base 不会影响已经创建的 lambda,因为捕获的是 base 当前指向的值(按名绑定)
    # 更安全的做法:用默认参数冻结
    custom_adder2 = lambda x, b=base*2: x + b   # 使用默认参数实现“值捕获”
    print("custom_adder2(5) =", custom_adder2(5))   # 205

if __name__ == "__main__":
    main()

总结与指引

要点说明
捕获列表是 Lambda 连接外部世界的桥梁[]表示不捕获,[=]全值,[&]全引用,可混合
值捕获默认只读需要修改副本时加 mutable,但外部变量不变
引用捕获可修改外部注意生命周期,避免悬空引用
初始化捕获(C++14)允许创建新变量或移动资源,如 [ptr = move(p)]
泛型 Lambda(C++14)参数用 auto,像模板一样适应多种类型
捕获 this类成员函数内访问成员变量/函数
递归 Lambda必须用 std::function 包装

接下来可以学习:

  • std::functionstd::bind 的用法
  • 高阶函数与函数式编程思想
  • C++17 的 constexpr Lambda
  • 生命周期管理:智能指针与 Lambda 配合

掌握了捕获列表,你就能写出像胶水一样黏合上下文与算法的 Lambda,让 STL 算法变得无比灵活。快去用 std::sort 配合捕获一个外部比较规则试试吧!

例题精讲

1单选题

考虑以下C++代码片段,关于Lambda捕获列表的说法,哪一个是正确的?

A[=]捕获所有外部变量,包括this指针
B[&]捕获所有外部变量,但this指针只能按值捕获
C[this]捕获当前对象的引用,可以修改成员变量
D[=, &x]表示除了x按引用捕获,其他变量按值捕获
2判断题

在C++中,使用默认捕获模式[=]的Lambda表达式内部可以修改按值捕获的变量,只要在Lambda参数列表后加上mutable关键字。

3填空题
以下代码试图使用Lambda对vector<int>进行排序,但需要捕获外部变量desc(bool类型)来决定降序还是升序。请补全捕获列表。
std::vector<int> v = {3, 1, 4, 1, 5};
bool desc = true;
std::sort(v.begin(), v.end(), [___](int a, int b) { return desc ? a > b : a < b; });
4单选题

关于Lambda的初始化捕获(C++14起支持),以下哪个用法是正确的?

A[x = std::move(obj)](){ ... } // 假设obj是局部变量
B[&x = obj](){ ... } // 引用捕获并初始化
C[x = 42](){ ... } // 捕获一个常量
D以上全部正确
5判断题

在C++中,一个Lambda表达式如果捕获了局部变量的引用,那么该Lambda对象可以安全地存储在全局容器中,即使该局部变量已经销毁。