C++ Lambda 从入门到实战

别急着复制代码,先搞清楚 Lambda 到底是什么、怎么用、为什么用


前言

如果你写过 C++,大概率遇到过这种场景:想给 std::sort 传个自定义比较规则,结果得专门写个结构体重载 operator();想在一个函数里临时搞个回调,又得去类外面定义一个全局函数。

C++11 引入的 Lambda 表达式 就是为了干掉这种“为了一碟醋包一顿饺子”的写法。

这篇文章我会从最基础的语法讲起,然后深入到捕获方式、泛型 Lambda、可变修饰,最后把那些面试常问、实际踩坑的问题一次性说清楚。


一、Lambda 长什么样

标准语法长这样:

[capture](parameters) -> return_type { body }

其中 -> return_type 可以省略(编译器能自动推导时),parameters 也可以为空(但括号不能省,除非是 C++14 的泛型形式)。

最简单的 Hello Lambda:

auto f = []() { std::cout << "hello lambda\n"; };
f();  // 输出

如果参数和返回值都很简单,可以写成:

auto add = [](int a, int b) { return a + b; };   // 返回值推导为 int
std::cout << add(3, 4); // 7

二、捕获列表:这是 Lambda 的灵魂

Lambda 最强大的地方在于可以捕获外部变量

2.1 值捕获 [=]

int x = 10;
auto f = [=]() { std::cout << x << '\n'; };  // 拷贝一份 x
x = 20;
f(); // 输出 10,因为捕获的是副本

2.2 引用捕获 [&]

int x = 10;
auto f = [&]() { std::cout << x << '\n'; };
x = 20;
f(); // 输出 20,引用的是同一个变量

2.3 显式指定

int a = 1, b = 2;
auto f = [a, &b]() { /* 可以读 a(值),可以读写 b(引用) */ };

2.4 混合使用

int a = 1, b = 2, c = 3;
auto f = [=, &b]() { /* a 和 c 值捕获,b 引用捕获 */ };
auto g = [&, a]() { /* b 和 c 引用捕获,a 值捕获 */ };

⚠️ 警告[=][&] 在 C++20 之前是“捕获所有”,但如果你在类成员函数里用,要小心 this 的捕获,后面会讲。


三、mutable:让值捕获也能改副本

默认情况下,值捕获的变量在 Lambda 体内是只读的(因为 operator() 默认是 const)。如果你需要修改副本,加 mutable

int count = 0;
auto f = [count]() mutable {
    count++;
    std::cout << count << '\n';
};
f(); // 1
f(); // 2
std::cout << count; // 0,外部原值不变

注意:mutable 必须写在参数列表后面,返回值类型前面(如果有的话)。


四、返回类型推导的坑

大多数情况下编译器能推导返回类型,但如果 body 里有多个 return 且类型不一致,或者你想显式指定,就用尾置返回类型。

auto f = [](int x) -> double {
    if (x > 0) return 1.0;
    else return 0;  // 0 会隐式转为 double
};

不显式指定的话,下面这种会编译报错(C++11 中):

auto bad = [](int x) {
    if (x) return 1;   // int
    else return 1.0;   // double,推导冲突
};
// C++14 之后可能按第一个推导,但最好别依赖

五、泛型 Lambda(C++14 起)

如果你想让 Lambda 接受任意类型,参数用 auto

auto add_any = [](auto a, auto b) { return a + b; };
std::cout << add_any(3, 5);          // int
std::cout << add_any(1.2, 3.4);      // double
std::cout << add_any(std::string("a"), std::string("b")); // string

这本质上是生成了一个模板化的 operator(),非常灵活。


六、捕获 this*this

在类成员函数里写 Lambda,如果要用到成员变量,捕获 this

class Foo {
    int m_val = 42;
public:
    void bar() {
        auto f = [this]() { std::cout << m_val; }; // 捕获 this 指针
        f();
    }
};

但这里有个隐患:如果 Lambda 被延迟执行(比如存到回调里),而 this 对象已经被销毁,那就悬垂指针了。

C++17 提供了 [*this],按值拷贝整个对象:

auto f = [*this]() { std::cout << m_val; }; // 拷贝副本,安全但开销可能大

七、Lambda 的本质是什么

编译器会把 Lambda 转换为一个匿名的函数对象(仿函数),类型是唯一的闭包类型(closure type)。

比如:

auto f = [x](int y) { return x + y; };

大致等价于:

class __AnonymousLambda {
    int x;
public:
    __AnonymousLambda(int _x) : x(_x) {}
    auto operator()(int y) const { return x + y; }
};
auto f = __AnonymousLambda(x);

这就是为什么:

  • 每个 Lambda 类型不同,哪怕长得一样
  • 值捕获的变量作为成员变量,mutable 控制 operator() 是否 const

八、什么时候用 std::function,什么时候用 auto

  • auto:绝大多数场景,性能最优,无类型擦除开销。
  • std::function:需要存储不同类型的可调用对象(比如放到容器里,或者作为多态回调)。
std::function<int(int)> func = [](int x) { return x * 2; };

std::function 会有堆分配和虚函数调用开销,能用 auto 就别用 function


九、常见“翻车”问题

9.1 捕获局部变量的引用,但 Lambda 生命周期超过局部变量

std::function<int()> make_func() {
    int local = 100;
    return [&]() { return local; }; // 危险!
}
auto f = make_func();
std::cout << f(); // 未定义行为

解决:改成 [=] 或者 [local]

9.2 在循环里捕获迭代变量

std::vector<std::function<void()>> tasks;
for (int i = 0; i < 3; ++i) {
    tasks.push_back([&]() { std::cout << i; }); // 全是同一个 i 的引用
}
for (auto& t : tasks) t(); // 可能输出 333 或其它

解决:捕获副本 [i] 或者 [=](确保 i 是按值)。

9.3 默认 [=] 捕获 this 的陷阱

在成员函数中,[=] 会隐式捕获 this(C++20 之前),你以为捕获了成员变量的值,实际上捕获的是指针。

class Foo {
    int x = 10;
public:
    void go() {
        auto f = [=]() { std::cout << x; }; // 捕获的是 this,不是 x 的副本
        // 如果对象销毁,调用 f 就崩
    }
};

C++20 废弃了 [=] 隐式捕获 this,建议显式写 [this][*this]


十、完整示例代码

下面这个 demo 覆盖了值捕获、引用捕获、mutable、泛型、返回值推导、以及安全使用场景,可以直接跑。

#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include<algorithm>

// 1. 基础:比较大小,用于 sort
void demo_sort() {
    std::vector<int> nums = {4, 2, 5, 1, 3};
    
    // 值捕获外部阈值,按降序比较
    int threshold = 3;
    std::sort(nums.begin(), nums.end(), [threshold](int a, int b) {
        return a > b;  // 降序
    });
    
    std::cout << "Sorted desc: ";
    for (int n : nums) std::cout << n << " ";
    std::cout << "\n";
}

// 2. mutable:计数器
void demo_mutable() {
    int base = 10;
    auto counter = [base]() mutable {
        static int call_count = 0;  // 注意 static 在 lambda 内部是跨调用的
        base += 1;
        return base;
    };
    std::cout << "mutable demo: " << counter() << ", " << counter() << "\n"; // 11, 12
    std::cout << "base outside: " << base << "\n"; // 10
}

// 3. 泛型 lambda + 返回值推导
void demo_generic() {
    auto concat_or_add = [](auto a, auto b) {
        return a + b;  // 如果 a,b 是 string,就拼接;是数值就相加
    };
    std::cout << "generic (int): " << concat_or_add(3, 4) << "\n";
    std::cout << "generic (string): " << concat_or_add(std::string("Hello "), "World") << "\n";
}

// 4. 避免悬垂引用:正确的捕获方式
std::function<int()> make_safe_func(int value) {
    // 按值捕获 value,安全
    return [value]() { return value * 2; };
}

void demo_safety() {
    auto f = make_safe_func(21);
    std::cout << "safe func: " << f() << "\n"; // 42
}

// 5. 成员函数中的 this 捕获(演示安全拷贝)
class Widget {
    int data = 100;
public:
    void run() {
        // 安全:拷贝整个对象(C++17)
        auto f = [*this]() mutable {
            data += 10;  // 修改的是副本
            std::cout << "inside lambda: " << data << "\n";
        };
        f();
        std::cout << "original data: " << data << "\n"; // 100
    }
};

int main() {
    demo_sort();
    demo_mutable();
    demo_generic();
    demo_safety();
    
    Widget w;
    w.run();
    
    return 0;
}

运行结果示例

Sorted desc: 5 4 3 2 1 
mutable demo: 11, 12
base outside: 10
generic (int): 7
generic (string): Hello World
safe func: 42
inside lambda: 110
original data: 100

十一、总结

场景 推荐写法
临时排序 / 算法回调 [][&] 按需捕获,优先 auto 存储
需要修改捕获的副本 mutable
异步回调 / 存储延迟执行 [=] 或显式值捕获,避免引用悬垂
成员函数中安全捕获 [*this](C++17)或显式 [this] 并确保生命周期
需要多态存储 使用 std::function,但注意性能损耗
泛型代码 使用 auto 参数(C++14)

Lambda 不是什么黑魔法,它只是语法糖,但用好了能让代码简洁直观,用错了就是隐晦 bug 的温床。核心记住一点:捕获的是引用还是副本,生命周期是否安全。 其余的都是细枝末节。

如果这篇文章帮到了你,欢迎点赞收藏,有问题评论区见。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐