Lambda 表达式是 C++11 引入的一项革命性特性,它极大地改变了 C++ 的编程风格,尤其是在与标准模板库(STL)配合使用时。它允许我们在代码中需要一个函数的地方,直接定义一个匿名的、临时的函数对象。


1. 什么是 Lambda 表达式?

简单来说,Lambda 表达式就是一个可调用的代码单元,可以把它理解为一个匿名的内联函数。与普通函数不同,它可以在函数内部定义,并且可以“捕获”其所在作用域中的变量。

2. 为什么需要 Lambda 表达式?

在 C++11 之前,如果想向一个算法(如 std::sort)传递一个简单的、一次性的比较逻辑,通常需要:

  1. 定义一个全局函数或静态成员函数:这会污染命名空间,并且逻辑与使用它的地方相隔甚远。
  2. 定义一个函数对象(Functor):需要编写一个完整的类,重载 operator()。这非常繁琐。

Lambda 的优势:

  • 代码局部性:将逻辑直接写在使用它的地方,代码更紧凑,可读性更高。
  • 简洁性:避免了编写独立的函数或函数对象类的样板代码。
  • 状态捕获:可以方便地访问和使用其定义作用域内的变量,这是普通函数难以做到的。

3. Lambda 表达式的完整语法

一个完整的 Lambda 表达式的语法结构如下:

1
2
3
[capture_list] (parameters) mutable noexcept -> return_type {
// function body
}

3.1. 捕获列表 [] (Capture Clause)

这是 Lambda 最重要和最强大的部分。它决定了 Lambda 函数体内部可以访问哪些外部变量,以及如何访问它们。

  • []不捕获任何外部变量

    1
    []() { std::cout << "Hello from Lambda!" << std::endl; }
  • [=]按值捕获所有外部变量。在 Lambda 内部,得到的是外部变量的一份拷贝。修改它们不会影响外部的原始变量。

    1
    2
    3
    4
    5
    6
    int x = 10;
    auto myLambda = [=]() {
    // x 在这里是 10 的一份拷贝
    std::cout << x << std::endl; // 合法
    // x = 20; // 编译错误!因为拷贝是 const 的
    };
  • [&]按引用捕获所有外部变量。在 Lambda 内部,得到的是外部变量的引用。修改它们会直接影响外部的原始变量。

    1
    2
    3
    4
    5
    6
    int x = 10;
    auto myLambda = [&]() {
    x = 20; // 合法,外部的 x 会被修改为 20
    };
    myLambda();
    std::cout << x; // 输出 20

    警告:按引用捕获要非常小心悬挂引用的问题!如果 Lambda 的生命周期超过了它所引用的局部变量,调用这个 Lambda 将导致未定义行为。

  • [this]:捕获当前对象的 this 指针。这允许你在 Lambda 内部访问类的成员变量和成员函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MyClass {
    public:
    void doWork() {
    int factor = 2;
    auto task = [this, factor](int value) {
    // this->member_var_ 是通过 this 指针访问的
    return value * factor * this->member_var_;
    };
    int result = task(5);
    }
    private:
    int member_var_ = 10;
    };
  • 指定捕获:可以精确控制捕获哪些变量以及如何捕获。

    • [x, &y]:变量 x 按值捕获,变量 y 按引用捕获。
    • [=, &y]:除了 y 按引用捕获,其他所有变量都按值捕获。
    • [&, x]:除了 x 按值捕获,其他所有变量都按引用捕获。

3.2. 参数列表 () (Parameter List)

和普通函数的参数列表一样,用于接收传递给 Lambda 的参数。如果不需要参数,() 可以省略。

1
2
3
4
auto add = [](int a, int b) {
return a + b;
};
int sum = add(3, 4); // sum is 7

3.3. mutable 关键字

默认情况下,对于按值捕获的变量,Lambda 内部不能修改它们(它们是 const 的)。如果你希望在 Lambda 内部可以修改这些拷贝,就需要使用 mutable 关键字。

1
2
3
4
5
6
7
int x = 10;
auto myLambda = [=]() mutable {
x = 20; // 合法,但修改的是内部的拷贝
std::cout << "Inside lambda, x = " << x << std::endl;
};
myLambda();
std::cout << "Outside lambda, x = " << x << std::endl;

输出:

1
2
Inside lambda, x = 20
Outside lambda, x = 10

3.4. 异常说明 noexcept

和普通函数一样,你可以用 noexcept 来指明该 Lambda 不会抛出异常,这有助于编译器进行优化。

1
auto f = []() noexcept { /* ... */ };

3.5. 返回类型 -> return_type (Trailing Return Type)

  • 自动推导:在大多数情况下,编译器可以根据 return 语句自动推导出 Lambda 的返回类型,所以 -> return_type可选的
    1
    auto add = [](int a, int b) { return a + b; }; // 编译器推导出返回类型为 int
  • 显式指定:如果 Lambda 函数体中有多个返回语句,且它们的类型不同,或者你希望强制返回一个特定类型(例如,return 0; 本应推导为 int,但你希望返回 long),则必须显式指定返回类型。
    1
    2
    3
    4
    5
    6
    7
    auto f = [](double d) -> int {
    if (d > 0) {
    return static_cast<int>(d); // 返回 int
    } else {
    return 0; // 返回 int
    }
    };

3.6. 函数体 {} (Function Body)

Lambda 的执行逻辑,和普通函数的函数体一样。


4. 高级用法

4.1. 泛型 Lambda (Generic Lambdas, C++14)

通过在参数列表中使用 auto 关键字,可以创建泛型 Lambda。这实际上是创建了一个函数模板。

1
2
3
4
5
6
7
auto add = [](auto a, auto b) {
return a + b;
};

int i = add(3, 4); // a, b 推导为 int
double d = add(3.5, 4.5); // a, b 推导为 double
std::string s = add(std::string("hello"), std::string(" world")); // a, b 推导为 std::string

4.2. 初始化捕获 / 广义捕获 (Init Capture, C++14)

这极大地增强了捕获列表的功能,允许你在捕获时进行初始化。语法是 [identifier = expression]

用途:

  1. 创建仅在 Lambda 内部可见的新变量

    1
    2
    3
    4
    5
    6
    int x = 10;
    auto myLambda = [y = x + 5]() {
    // y 是在捕获列表中创建的,值为 15
    // x 在这里是不可见的
    std::cout << y << std::endl; // 输出 15
    };
  2. 移动捕获(Move Capture):对于只能移动不能拷贝的类型(如 std::unique_ptr),这是捕获它们的唯一方式。

    1
    2
    3
    4
    5
    6
    auto ptr = std::make_unique<int>(10);
    auto myLambda = [p = std::move(ptr)]() {
    std::cout << "Value from unique_ptr: " << *p << std::endl;
    };
    myLambda();
    // 此时 ptr 已经是 nullptr 了

4.3. constexpr Lambda (C++17)

如果一个 Lambda 满足 constexpr 函数的所有要求,你可以在其前面加上 constexpr 关键字,使其可以在编译时求值。

1
2
constexpr auto add = [](int a, int b) { return a + b; };
static_assert(add(2, 3) == 5, "Compile-time check failed");

5. 实际应用场景

5.1. 与 STL 算法结合

这是 Lambda 最常见的用途。

  • std::sort

    1
    2
    3
    4
    std::vector<int> v = {5, 1, 4, 2, 3};
    std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b; // 实现降序排序
    });
  • std::for_each

    1
    2
    3
    4
    5
    std::vector<int> v = {1, 2, 3};
    int sum = 0;
    std::for_each(v.begin(), v.end(), [&sum](int n) {
    sum += n;
    }); // sum 最终为 6
  • std::find_if

    1
    2
    3
    4
    5
    6
    7
    std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
    auto it = std::find_if(names.begin(), names.end(), [](const std::string& name) {
    return name.length() > 4;
    });
    if (it != names.end()) {
    std::cout << "Found: " << *it << std::endl; // 输出 "Found: Alice"
    }

5.2. 作为 std::function 的实现

std::function 是一个通用的可调用对象包装器。Lambda 可以被用来初始化 std::function 对象。

1
2
3
4
5
6
7
8
std::function<int(int, int)> operation;
int choice = 1;
if (choice == 1) {
operation = [](int a, int b) { return a + b; };
} else {
operation = [](int a, int b) { return a * b; };
}
int result = operation(5, 3);

5.3. 多线程编程

在创建线程时,Lambda 提供了一种非常方便的方式来定义线程要执行的任务。

1
2
3
4
5
6
7
8
#include <thread>
#include <string>

std::string message = "Hello from thread";
std::thread t([message]() { // 按值捕获 message,避免数据竞争
std::cout << message << std::endl;
});
t.join();

6. 底层原理 (Lambda 是如何工作的)

在编译时,编译器会将每个 Lambda 表达式转换成一个唯一的、匿名的类类型,这个类被称为闭包类型(Closure Type)

一个 Lambda 表达式 [...] (...) {...} 基本上等同于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class __UniqueLambdaName {
public:
// 1. 捕获的变量成为类的成员变量
// 按值捕获是普通成员,按引用捕获是引用成员
// 例如,[x, &y]
int m_x;
int& m_y;

// 2. 构造函数用于初始化捕获的成员
__UniqueLambdaName(int x, int& y) : m_x(x), m_y(y) {}

// 3. operator() 被重载,其函数体就是 Lambda 的函数体
// 默认是 const 方法,除非使用了 mutable
auto operator()(/* parameters */) const { // 或 mutable 时为非 const
// ... Lambda 的函数体 ...
}
};

当你定义一个 Lambda 时,编译器会创建这个类的一个实例,这个实例就是闭包对象(Closure Object)。这就是为什么 Lambda 可以存储状态(通过捕获的成员变量)并且可以被调用的原因。


7. 总结

特性 语法/关键字 描述 引入版本
基本 Lambda [](){} 创建匿名函数对象 C++11
值捕获 [=], [x] 拷贝外部变量 C++11
引用捕获 [&], [&x] 引用外部变量 C++11
修改值捕获 mutable 允许修改按值捕获的拷贝 C++11
泛型 Lambda [](auto p){} 参数类型自动推导,类似模板 C++14
初始化捕获 [x=expr] 在捕获时创建并初始化变量 C++14
编译时求值 constexpr 允许 Lambda 在编译时执行 C++17