咸宁燃冰世纪教育

C++ Lambda表达式
完全指南

探索现代C++中最强大的特性之一,简洁、灵活且功能强大的匿名函数

什么是Lambda表达式?

Lambda表达式(也称为匿名函数)是C++11引入的一项强大特性,它允许你在需要函数的地方直接定义函数,而无需单独声明。这种"即写即用"的方式使代码更加简洁、可读,尤其适合编写简短的函数或回调。

为什么使用Lambda表达式?

  • 减少样板代码,无需单独定义命名函数
  • 将函数定义在使用的地方,提高代码可读性
  • 方便捕获周围环境的变量,与上下文交互
  • 非常适合作为算法函数的参数(如排序、查找的谓词)

Lambda表达式在现代C++编程中被广泛使用,特别是在STL算法、并发编程和事件处理等场景中。掌握Lambda表达式是成为高效C++开发者的关键技能之一。

一个简单的Lambda表达式示例

基本Lambda示例
#include <iostream>

int main() {
    // 这是一个简单的lambda表达式,打印"Hello, Lambda!"
    []() { 
        std::cout << "Hello, Lambda!" << std::endl; 
    }(); // 立即调用lambda表达式
    
    return 0;
}

运行结果:Hello, Lambda!

Lambda表达式常用场景

Lambda表达式的语法结构

Lambda表达式的基本语法如下,包含几个可选部分:

Lambda语法结构
[](/* 参数列表 */) /*  mutable(可选) */ /* -> 返回类型(可选) */ {
    // 函数体
}

1. 捕获子句 []

定义Lambda表达式可以访问的外部变量,位于Lambda表达式的开头。

  • [] - 不捕获任何外部变量
  • [=] - 按值捕获所有外部变量
  • [&] - 按引用捕获所有外部变量
  • [x] - 按值捕获x
  • [&x] - 按引用捕获x
  • [=, &x] - 除x按引用,其余按值

2. 参数列表 ()

与普通函数的参数列表类似,指定Lambda表达式的输入参数。

// 带参数的lambda
[](int a, int b) {
    return a + b;
}

3. 可变规范 mutable

允许修改按值捕获的变量(默认情况下不能修改)。

int x = 0;
[x]() mutable {
    x++; // 允许修改,因为有mutable
    std::cout << x;
}();

4. 返回类型 -> type

显式指定Lambda表达式的返回类型。如果函数体只有一个return语句,可省略。

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

5. 函数体 {}

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

// 复杂一点的lambda函数体
[](int n) {
    if (n <= 1) return 1;
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

Lambda表达式的特性

变量捕获详解

变量捕获是Lambda表达式最强大的特性之一,它允许Lambda访问定义它的作用域中的变量。有多种捕获方式,适用于不同场景:

捕获方式 说明 能否修改
[] 不捕获任何变量 -
[=] 按值捕获所有外部变量 默认不能,需加mutable
[&] 按引用捕获所有外部变量 可以修改
[x] 按值捕获x 默认不能,需加mutable
[&x] 按引用捕获x 可以修改
[=, &x] 除x按引用,其余按值 x可以修改,其他默认不能
变量捕获示例
#include <iostream>

int main() {
    int a = 10, b = 20;
    
    // 按值捕获a,按引用捕获b
    [a, &b]() {
        std::cout << "a = " << a << ", b = " << b << std::endl;
        b = 30; // 可以修改引用捕获的变量
        // a = 5; 错误!不能修改按值捕获的变量
    }();
    
    std::cout << "修改后的b = " << b << std::endl;
    
    // 使用mutable允许修改按值捕获的变量
    [a]() mutable {
        std::cout << "修改前的a(副本) = " << a << std::endl;
        a = 5;  // 现在允许修改了
        std::cout << "修改后的a(副本) = " << a << std::endl;
    }();
    
    std::cout << "原始a = " << a << std::endl; // 原始a不变
    
    return 0;
}

运行结果:
a = 10, b = 20
修改后的b = 30
修改前的a(副本) = 10
修改后的a(副本) = 5
原始a = 10

Lambda表达式的类型

Lambda表达式有一个独特的、编译器生成的类型,这个类型没有名字(匿名),因此我们无法直接声明这种类型的变量。但我们可以使用auto关键字来存储Lambda表达式:

存储Lambda表达式
auto add = [](int a, int b) {
    return a + b;
};

int result = add(3, 5); // result = 8

对于需要存储Lambda表达式的场景(如作为类成员),可以使用std::function

使用std::function存储Lambda
#include <functional>

// 声明一个函数对象
std::function<int(int, int)> operation;

// 赋值lambda表达式
operation = [](int a, int b) {
    return a * b;
};

int product = operation(4, 5); // product = 20

泛型Lambda(C++14及以上)

C++14引入了泛型Lambda,允许使用auto作为参数类型,使Lambda表达式更加灵活:

泛型Lambda示例
// 泛型lambda,可以接受任何类型的参数
auto print = [](auto x) {
    std::cout << x << std::endl;
};

print(42);          // 打印整数
print("Hello");   // 打印字符串
print(3.14159);      // 打印浮点数

// 更复杂的泛型lambda
auto sum = [](auto a, auto b) {
    return a + b;
};

int i = sum(10, 20);          // 30
double d = sum(1.5, 2.5);  // 4.0
std::string s = sum("Hello ", "World"); // "Hello World"

Lambda表达式实例演示

1. 与STL算法一起使用

Lambda表达式非常适合作为STL算法的谓词参数,使代码更加简洁:

STL算法中的Lambda
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {5, 2, 9, 1, 5, 6};
    
    // 使用lambda排序(降序)
    std::sort(numbers.begin(), numbers.end(),
        [](int a, int b) {
            return a > b; // 降序排序
        });
    
    // 使用lambda查找第一个偶数
    auto even_it = std::find_if(numbers.begin(), numbers.end(),
        [](int n) {
            return n % 2 == 0;
        });
    
    if (even_it != numbers.end()) {
        std::cout << "第一个偶数是: " << *even_it << std::endl;
    }
    
    // 使用lambda计算所有奇数的和
    int odd_sum = 0;
    std::for_each(numbers.begin(), numbers.end(),
        [&odd_sum](int n) {
            if (n % 2 != 0) {
                odd_sum += n;
            }
        });
    
    std::cout << "所有奇数的和是: " << odd_sum << std::endl;
    
    return 0;
}

运行结果:
第一个偶数是: 6
所有奇数的和是: 15

2. 作为函数返回值

Lambda表达式可以作为函数的返回值,创建工厂模式的函数生成器:

返回Lambda的函数
#include <iostream>
#include <functional>

// 返回一个lambda表达式,用于比较数是否大于阈值
std::function<bool(int)> make_greater_than_checker(int threshold) {
    // 按值捕获threshold
    return [threshold](int x) {
        return x > threshold;
    };
}

int main() {
    // 创建一个检查是否大于10的函数
    auto greater_than_10 = make_greater_than_checker(10);
    
    std::cout << "15 > 10? " << (greater_than_10(15) ? "是" : "否") << std::endl;
    std::cout << "5 > 10? " << (greater_than_10(5) ? "是" : "否") << std::endl;
    
    // 创建一个检查是否大于20的函数
    auto greater_than_20 = make_greater_than_checker(20);
    std::cout << "25 > 20? " << (greater_than_20(25) ? "是" : "否") << std::endl;
    
    return 0;
}

3. 在多线程中使用

Lambda表达式在多线程编程中非常有用,可以方便地传递线程函数:

多线程中的Lambda
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

std::mutex mtx; // 互斥锁,用于同步输出

int main() {
    std::vector<std::thread> threads;
    
    // 创建5个线程
    for (int i = 0; i < 5; ++i) {
        // 使用lambda作为线程函数,捕获i的副本
        threads.emplace_back([i]() {
            std::lock_guard<std::mutex> lock(mtx);
            std::cout << "线程 " << i << " 正在运行" << std::endl;
        });
    }
    
    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

4. 嵌套Lambda表达式

Lambda表达式可以嵌套使用,创建更复杂的逻辑结构:

嵌套Lambda表达式
#include <iostream>
#include <functional>

int main() {
    // 外部lambda:创建一个计算操作的函数
    auto create_calculator = []() {
        int count = 0; // 计数器,跟踪计算次数
        
        // 内部lambda:实际的计算函数
        return [count](int a, int b, char op) mutable -> int {
            count++; // 每次调用递增计数器
            switch(op) {
                case '+': return a + b;
                case '-': return a - b;
                case '*': return a * b;
                case '/': return b != 0 ? a / b : 0;
                default: return 0;
            }
        };
    };
    
    // 创建计算器
    auto calculator = create_calculator();
    
    // 使用计算器
    std::cout << "3 + 5 = " << calculator(3, 5, '+') << std::endl;
    std::cout << "10 * 4 = " << calculator(10, 4, '*') << std::endl;
    std::cout << "8 - 2 = " << calculator(8, 2, '-') << std::endl;
    
    return 0;
}

Lambda表达式最佳实践

推荐做法

  • 用于简短的函数逻辑,通常不超过几行代码
  • 作为STL算法的参数(如sort、find_if等)
  • 捕获变量时尽量明确指定(避免使用[=]或[&]捕获所有变量)
  • 需要访问局部变量的小型回调函数
  • 使用auto存储Lambda表达式,提高代码简洁性

避免做法

  • 用于复杂逻辑(超过10行代码),此时应使用命名函数
  • 过度使用[=]或[&]捕获所有变量,可能导致意外行为
  • 长时间存储Lambda表达式(如作为全局变量)
  • 在需要函数指针的地方使用Lambda(除非无捕获)
  • 过度嵌套Lambda表达式,降低代码可读性

常见陷阱与解决方案

1. 捕获循环变量的陷阱

在C++11中,按值捕获循环变量可能导致所有Lambda共享同一个变量的副本:

// C++11中的陷阱
for (int i = 0; i < 3; ++i) {
    // 所有lambda捕获的是同一个i的副本
    [i]() { std::cout << i << " "; }();
}

解决方案:在C++14及以上版本中,这个问题已修复,每次迭代都会捕获当前值。对于C++11,可以创建一个局部变量:

for (int i = 0; i < 3; ++i) {
    int j = i; // 创建局部变量
    [j]() { std::cout << j << " "; }();
}

2. 引用捕获的生命周期问题

引用捕获的变量如果在Lambda表达式使用前被销毁,会导致未定义行为:

auto create_function() {
    int x = 10;
    return [&x]() { return x; }; // 危险!x将被销毁
}

auto func = create_function();
int value = func(); // 未定义行为!x已不存在

解决方案:避免引用捕获将要销毁的局部变量,改用按值捕获。

总结

Lambda表达式是现代C++中一项非常有价值的特性,它提供了一种简洁、灵活的方式来定义匿名函数。通过本文的学习,你应该已经掌握了:

  • Lambda表达式的基本语法和结构
  • 变量捕获的各种方式及其用途
  • 如何在实际编程中应用Lambda表达式
  • 使用Lambda表达式的最佳实践和注意事项

熟练掌握Lambda表达式可以使你的C++代码更加简洁、优雅和高效,特别是在使用STL算法、多线程编程和事件处理等场景中。

记住,虽然Lambda表达式非常强大,但也不要过度使用。对于复杂的逻辑,使用命名函数往往更有利于代码的可读性和维护性。平衡使用各种编程工具,才能编写出高质量的代码。