自 C++11 标准引入以来,Lambda 表达式(也称为匿名函数)已经成为现代 C++ 编程中不可或缺的一部分。它们提供了一种简洁而强大的方式来定义内联函数,特别适用于需要临时函数对象的场景,如算法库的使用。这次整理,我尽可能全面深入地探讨 C++ 中的 Lambda 表达式,包括其语法、特性、用法和实现原理。


一、什么是 Lambda 表达式

Lambda 表达式是一种 匿名函数,可以在代码中定义并立即使用,而无需为其命名。这使得代码更加简洁和可读,特别是在需要将函数作为参数传递的情况下。

Lambda 表达式的特点

  • 简洁性:无需定义额外的函数或函数对象类。
  • 就近性:函数定义与使用地点接近,方便阅读和维护。
  • 强大性:支持捕获外部变量,能够访问所在作用域的上下文。

基本语法

1
2
3
[capture list](parameter list) -> return type {
    function body
};
  • capture list(捕获列表):指定哪些外部变量可在 Lambda 表达式中使用,以及如何捕获。
  • parameter list(参数列表):与普通函数的参数列表类似,指定传递给 Lambda 的参数。
  • return type(返回类型):可选,使用尾置返回类型(trailing return type)语法。
  • function body(函数体):Lambda 表达式的执行代码。

示例

1
2
3
[](const int& no) -> void {
    std::cout << "编号 " << no << ": 你好,Lambda!\n";
};

二、Lambda 表达式的使用示例

让我们通过一个完整的示例来理解 Lambda 表达式的实际应用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <vector>
#include <algorithm>

void show(const int& no) {
    std::cout << "普通函数:编号 " << n << "\n";
}

int main() {
    std::vector<int> numbers = {1, 2, 3};

    // 使用普通函数
    std::for_each(numbers.begin(), numbers.end(), show);

    // 使用函数对象(仿函数)
    struct Functor {
        void operator()(int n) const {
            std::cout << "仿函数:编号 " << n << "\n";
        }
    };
    std::for_each(numbers.begin(), numbers.end(), Functor());

    // 使用 Lambda 表达式
    std::for_each(numbers.begin(), numbers.end(), [](int n) {
        std::cout << "Lambda 表达式:编号 " << n << "\n";
    });

    return 0;
}

输出:

1
2
3
4
5
6
7
8
9
普通函数:编号 1
普通函数:编号 2
普通函数:编号 3
仿函数:编号 1
仿函数:编号 2
仿函数:编号 3
Lambda 表达式:编号 1
Lambda 表达式:编号 2
Lambda 表达式:编号 3

三、Lambda 表达式的各个部分详解

3.1 捕获列表(Capture List)

捕获列表位于 [] 中,用于指定 Lambda 表达式可以访问的外部变量。

 捕获方式

  • 值捕获(By Value)[a, b],拷贝外部变量的值。
  • 引用捕获(By Reference)[&a, &b],引用外部变量。
  • 隐式捕获
    • 按值捕获所有外部变量[=]
    • 按引用捕获所有外部变量[&]
  • 混合捕获[=, &a, &b][&, a, b]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int x = 10;
int y = 20;

// 值捕获
auto lambda_by_value = [x, y]() {
    // x 和 y 的值是在 Lambda 创建时拷贝的
    std::cout << "值捕获:x = " << x << ", y = " << y << "\n";
};

// 引用捕获
auto lambda_by_ref = [&x, &y]() {
    // x 和 y 是引用,指向原始变量
    x += 10;
    y += 10;
    std::cout << "引用捕获:x = " << x << ", y = " << y << "\n";
};

lambda_by_value(); // 输出:x = 10, y = 20
lambda_by_ref();   // 输出:x = 20, y = 30

std::cout << "外部变量:x = " << x << ", y = " << y << "\n"; // 输出:x = 20, y = 30

注意:

  • 值捕获的变量在 Lambda 创建时被拷贝,后续对原始变量的修改不会影响 Lambda 内部的值。
  • 引用捕获的变量,Lambda 内部的修改会影响外部变量。

3.2 参数列表(Parameter List)

Lambda 表达式的参数列表与普通函数类似,但有以下几点需要注意:

  • 必须为每个参数命名:不能省略参数名。
  • 不支持默认参数:无法为参数指定默认值。
  • 不支持可变参数:无法使用 ... 可变参数列表。
1
2
3
4
5
auto sum = [](int a, int b) {
    return a + b;
};

std::cout << "Sum: " << sum(3, 4) << "\n"; // 输出:Sum: 7

3.3 返回类型(Return Type)

Lambda 表达式可以显式指定返回类型,使用尾置返回类型语法 -> return_type。如果省略,编译器会自动推导。

1
2
3
4
5
6
7
8
9
// 编译器自动推导返回类型
auto multiply = [](int a, int b) {
    return a * b;
};

// 显式指定返回类型为 double
auto divide = [](int a, int b) -> double {
    return static_cast<double>(a) / b;
};

建议:

  • 对于复杂的返回类型或多种返回路径,显式指定返回类型以避免推导错误。

3.4 函数体(Function Body)

函数体包含 Lambda 表达式要执行的代码,与普通函数的函数体类似。

1
2
3
4
5
auto greet = []() {
    std::cout << "Hello, Lambda!\n";
};

greet(); // 输出:Hello, Lambda!

四、捕获列表的深入解析

4.1 值捕获(By Value)

  • 特性:捕获时拷贝变量的值,Lambda 内部的修改不会影响外部变量。
  • 限制:默认情况下,Lambda 内部不能修改值捕获的变量(除非使用 mutable 关键字)。
1
2
3
4
5
6
7
8
9
int count = 0;
auto increment = [count]() mutable {
    // 需要 mutable 才能修改值捕获的变量
    count++;
    std::cout << "内部 count = " << count << "\n";
};

increment(); // 输出:内部 count = 1
std::cout << "外部 count = " << count << "\n"; // 输出:外部 count = 0

4.2 引用捕获(By Reference)

  • 特性:捕获外部变量的引用,Lambda 内部的修改会影响外部变量。
  • 注意:需要确保外部变量在 Lambda 表达式执行时仍然存在。
1
2
3
4
5
6
7
8
int count = 0;
auto increment = [&count]() {
    count++;
    std::cout << "内部 count = " << count << "\n";
};

increment(); // 输出:内部 count = 1
std::cout << "外部 count = " << count << "\n"; // 输出:外部 count = 1

4.3 隐式捕获(Implicit Capture)

  • 按值捕获所有外部变量[=]
  • 按引用捕获所有外部变量[&]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int a = 5, b = 10;

// 按值捕获所有变量
auto lambda_value = [=]() {
    std::cout << "按值捕获:a = " << a << ", b = " << b << "\n";
};

// 按引用捕获所有变量
auto lambda_ref = [&]() {
    std::cout << "按引用捕获:a = " << a << ", b = " << b << "\n";
};

4.4 混合捕获(Mixed Capture)

  • 格式[default_capture, variable_list]
  • 规则:默认捕获方式和显式捕获方式不能对同一变量使用。
1
2
3
4
5
6
7
8
9
int x = 1, y = 2, z = 3;

// 默认按值捕获,显式按引用捕获 x
auto lambda_mixed = [=, &x]() {
    x++;
    std::cout << "x = " << x << ", y = " << y << ", z = " << z << "\n";
};

lambda_mixed(); // 输出:x = 2, y = 2, z = 3

4.5 修改值捕获的变量:mutable 关键字

  • 作用:允许在 Lambda 内部修改值捕获的变量。
  • 限制:修改仅在 Lambda 内部有效,不会影响外部变量。
1
2
3
4
5
6
7
8
int value = 5;
auto modify = [value]() mutable {
    value *= 2;
    std::cout << "内部 value = " << value << "\n";
};

modify(); // 输出:内部 value = 10
std::cout << "外部 value = " << value << "\n"; // 输出:外部 value = 5

五、Lambda 表达式的高级特性

5.1 异常说明和 noexcept

Lambda 表达式可以指定异常说明,以声明可能抛出的异常类型。

1
2
3
auto safe_divide = [](int a, int b) noexcept -> double {
    return static_cast<double>(a) / b;
};

5.2 Lambda 表达式的类型和实现

  • 类型:每个 Lambda 表达式都有唯一的匿名类型,不能被复制或赋值。
  • 实现原理:编译器将 Lambda 表达式转换为一个拥有 operator() 的匿名类。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto lambda = [](int n) {
    return n * n;
};

// 等价于
class __LambdaUniqueName__ {
public:
    int operator()(int n) const {
        return n * n;
    }
};

六、Lambda 表达式的实际应用场景

6.1 标准算法库的使用

Lambda 表达式在使用 STL 算法时非常方便,例如 std::sortstd::for_each

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
std::vector<int> numbers = {3, 1, 4, 1, 5};

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

for (int n : numbers) {
    std::cout << n << " ";
}
// 输出:5 4 3 1 1

6.2 事件处理和回调函数

在需要传递回调函数的场景,如 GUI 编程或异步操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void setCallback(std::function<void(int)> callback) {
    // 模拟事件触发
    callback(42);
}

int main() {
    setCallback([](int eventId) {
        std::cout << "事件触发,ID:" << eventId << "\n";
    });
    return 0;
}

七、注意事项和最佳实践

  • 避免捕获过多的变量:捕获列表应尽量精简,明确指定需要的变量。
  • 显式指定返回类型:在可能有歧义或复杂返回类型时,显式指定返回类型。
  • 谨慎使用引用捕获:确保引用的变量在 Lambda 执行时仍然有效。
  • 理解可变性:使用 mutable 时,注意值捕获的变量修改仅在 Lambda 内部有效。