C++ 中的 Lambda 表达式(Lambda Expression)是 C++11 引入的一项重大特性,它允许你在代码中直接定义匿名函数对象。这使得代码更加简洁,特别是在需要传递短小的函数逻辑给算法(如 std::sortstd::for_each)或作为回调函数时。

1. 基本语法结构

Lambda 表达式的标准语法如下:

[捕获列表] (参数列表) mutable(可选) -> 返回类型(可选) { 函数体 }

  • [捕获列表]:指定如何从外部作用域捕获变量(这是 Lambda 最强大的功能)。
  • (参数列表):与普通函数一样,定义输入参数。如果无参数,可省略括号 []
  • mutable:默认情况下,按值捕获的变量在 Lambda 内是 const 的。加上 mutable 后,允许修改按值捕获的副本。
  • -> 返回类型:尾随返回类型。通常编译器可以自动推导,但在复杂逻辑或无返回值(需显式写 void)时可省略或显式指定。
  • { 函数体 }:具体的执行逻辑。

2. 捕获列表(Capture Clause)详解

捕获列表决定了 Lambda 内部如何访问外部变量。

语法 含义
[] 不捕获任何外部变量。
[x, y] 按值捕获 xy。Lambda 内部拥有它们的副本,修改不影响外部。
[&x, &y] 按引用捕获 xy。Lambda 内部直接操作外部变量,修改会影响外部。
[=] 按值捕获当前作用域内的所有自动变量。
[&] 按引用捕获当前作用域内的所有自动变量。
[&, x] 默认按引用捕获所有,但 x 按值捕获。
[=, &y] 默认按值捕获所有,但 y 按引用捕获。
[this] 捕获当前对象的 this 指针(用于成员函数中)。
[*this] (C++17) 按值捕获当前对象的所有成员。

重要注意事项:悬空引用

如果使用 [&] 或 [&var] 按引用捕获局部变量,必须确保 Lambda 的生命周期不超过被捕获变量的生命周期。如果 Lambda 被存储并在外部变量销毁后执行,会导致未定义行为(崩溃)。

3. 代码示例

示例 1:简单的按值与按引用捕获
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    int factor = 10;
    std::vector<int> nums = {1, 2, 3, 4, 5};

    // 按值捕获 factor:Lambda 内部使用的是 factor 的副本
    auto by_value = [factor](int n) { return n * factor; };

    // 按引用捕获 factor:如果 factor 改变,Lambda 结果也会变
    auto by_ref = [&factor](int n) { return n * factor; };

    std::cout << "初始 factor: " << factor << std::endl;
    std::cout << "按值计算 (5 * 10): " << by_value(5) << std::endl;

    factor = 20; // 修改外部变量

    std::cout << "修改后 factor: " << factor << std::endl;
    std::cout << "按值计算 (5 * 10): " << by_value(5) << std::endl; // 仍然是 50
    std::cout << "按引用计算 (5 * 20): " << by_ref(5) << std::endl; // 变成 100

    return 0;
}
示例 2:配合 STL 算法使用

这是 Lambda 最常见的场景,替代了以前需要编写独立函数或函数对象(Functor)的做法。

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {5, 2, 9, 1, 5, 6};
    int threshold = 4;

    // 排序:降序
    std::sort(vec.begin(), vec.end(), [](int a, int b) {
        return a > b;
    });

    // 查找第一个大于 threshold 的元素
    auto it = std::find_if(vec.begin(), vec.end(), [threshold](int n) {
        return n > threshold;
    });

    if (it != vec.end()) {
        std::cout << "找到第一个大于 " << threshold << " 的数: " << *it << std::endl;
    }

    // 遍历并修改:将所有偶数加倍 (需要 mutable 因为按值捕获的副本默认是 const,但这里没捕获外部变量,只是修改参数n的副本无效,若要修改容器需引用)
    // 正确做法:直接修改容器元素,通常用引用参数
    std::for_each(vec.begin(), vec.end(), [](int &n) {
        if (n % 2 == 0) n *= 2;
    });

    return 0;
}
示例 3:mutable 关键字的使用

当你按值捕获变量,却需要在 Lambda 内部修改这个副本时,必须使用 mutable

#include <iostream>

int main() {
    int count = 0;
    
    // 错误示范:没有 mutable,无法修改按值捕获的 count_copy
    // auto lambda_err = [count]() { count++; }; 

    // 正确示范
    auto lambda_ok = [count]() mutable { 
        count++; 
        std::cout << "内部副本计数: " << count << std::endl; 
    };

    lambda_ok(); // 输出 1
    lambda_ok(); // 输出 2 (状态保持在 Lambda 对象内部)
    
    std::cout << "外部原始计数: " << count << std::endl; // 输出 0,外部未受影响

    return 0;
}
示例 4:泛型 Lambda (C++14 及以上)

在 C++14 中,参数列表可以使用 auto,使得 Lambda 成为模板函数。

#include <iostream>

int main() {
    // 泛型 Lambda,编译器会自动推导 T
    auto add = [](auto a, auto b) {
        return a + b;
    };

    std::cout << add(1, 2) << std::endl;       // int
    std::cout << add(1.5, 2.5) << std::endl;   // double
    std::cout << add(std::string("Hi"), " Mom") << std::endl; // string

    return 0;
}

4. Lambda 的本质

在底层,Lambda 表达式实际上是语法糖。编译器会将 Lambda 转换为一个唯一的匿名类(闭包对象),该类重载了 operator()

例如:

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

大致等价于编译器生成的如下代码(简化版):

class __lambda_unique_name__ {
    int x; // 捕获的成员变量
public:
    __lambda_unique_name__(int val) : x(val) {} // 构造函数
    int operator()(int y) const { // 重载调用运算符
        return x + y;
    }
};

// 使用时:
__lambda_unique_name__ f(x);
f(5);

正因为如此,你可以使用 decltype 获取 Lambda 的类型,或者将 Lambda 存储在 std::function 中(虽然会有轻微的性能开销,但提供了多态性和可存储性)。

#include <functional>
#include <iostream>

int main() {
    auto lam = [](int i) { return i * 2; };
    
    // 存储到 std::function
    std::function<int(int)> func = lam;
    
    std::cout << func(10) << std::endl; // 输出 20
    return 0;
}

5. 最佳实践总结

  1. 优先使用自动推导:除非必要(如递归或需要特定返回类型),否则让编译器推导返回类型。
  2. 小心捕获列表
    • 如果在多线程环境或异步回调中使用,尽量避免 [&],防止悬空引用。
    • 明确列出需要的变量(如 [x, &y])通常比隐式捕获([=][&])更安全且可读性更好。
  3. 性能考量
    • Lambda 本身几乎没有运行时开销(通常会被内联优化)。
    • 但如果将其存入 std::function,会引入动态分配和虚函数调用的开销。在对性能极度敏感的场景(如高频交易、游戏循环热点),尽量直接使用 Lambda 对象或模板传递,避免 std::function
  4. 可读性:如果 Lambda 逻辑超过 3-5 行,或者逻辑非常复杂,建议将其提取为独立的命名函数,以保持代码清晰。

更多推荐