Lambda表达式

Lambda表达式


1. 什么是Lambda表达式?

通俗理解 :Lambda表达式是一种匿名函数(未命名的函数对象),你可以在需要函数的地方就地定义它,而无需单独写一个命名函数或函数对象。它让代码更简洁、更灵活,尤其适用于短小的、一次性使用的函数逻辑,比如作为参数传递给算法(如std::sort, std::for_each)。

核心思想:就地定义,即时使用。


2. Lambda表达式的基本语法

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

cpp 复制代码
[capture-list] (parameters) mutable -> return-type {
    // 函数体
}

其中,只有 [capture-list]{ 函数体 } 是必须的,其他部分在特定情况下可以省略。一个最简单的Lambda如下:

cpp 复制代码
[]{ std::cout << "Hello, Lambda!" << std::endl; }();
// 输出: Hello, Lambda!
// 注意最后的 `()` 是直接调用这个Lambda

让我们来分解它的各个部分。


3. 捕获列表 (Capture List) [...]

这是Lambda最独特和强大的特性之一。它定义了Lambda函数体内部如何访问其外部作用域中的变量。

a) 值捕获 [var]

将外部变量的值复制一份到Lambda内部。Lambda内部修改这个副本不会影响外部的原变量。

cpp 复制代码
int main() {
    int x = 10;
    auto lambda_val = [x] { // 捕获 x 的值
        std::cout << "Inside lambda (by value): " << x << std::endl;
        // x++; // 错误!默认情况下,值捕获的变量是 const 的,不可修改
    };
    x = 20; // 修改外部 x
    lambda_val(); // 调用Lambda
    std::cout << "Outside lambda: " << x << std::endl;
    return 0;
}

输出:

复制代码
Inside lambda (by value): 10
Outside lambda: 20

解释 :Lambda在创建的那一刻就复制了x的值(10)。之后外部x变为20,但Lambda内部使用的仍然是它创建时捕获的副本10。

b) 引用捕获 [&var]

捕获外部变量的引用。在Lambda内部操作这个变量就是在操作外部的原变量。

cpp 复制代码
int main() {
    int x = 10;
    auto lambda_ref = [&x] { // 捕获 x 的引用
        std::cout << "Inside lambda (by ref): " << x << std::endl;
        x++; // 可以修改,因为它是引用
    };
    lambda_ref(); // 调用Lambda
    std::cout << "Outside lambda: " << x << std::endl; // x 被修改了
    return 0;
}

输出:

复制代码
Inside lambda (by ref): 10
Outside lambda: 11
c) 隐式捕获

让编译器根据我们在函数体中的使用,自动推断需要捕获哪些变量。

  • [=]:以值捕获的方式捕获所有外部变量。
  • [&]:以引用捕获的方式捕获所有外部变量。
cpp 复制代码
int main() {
    int a = 1, b = 2;
    auto lambda_implicit = [=] { // 以值方式捕获所有需要的变量(a 和 b)
        std::cout << "a + b = " << a + b << std::endl;
    };
    auto lambda_implicit_ref = [&] { // 以引用方式捕获所有需要的变量
        a++;
        b++;
    };
    lambda_implicit();
    lambda_implicit_ref();
    std::cout << "a: " << a << ", b: " << b << std::endl;
    return 0;
}

输出:

复制代码
a + b = 3
a: 2, b: 3

最佳实践 :尽量避免使用隐式捕获 [=][&],显式地列出需要捕获的变量可以使代码更清晰、更安全。

d) 混合捕获

你可以混合使用值和引用捕获,以及隐式捕获。

  • [=, &var]:默认以值方式捕获所有变量,但变量var以引用方式捕获。
  • [&, var]:默认以引用方式捕获所有变量,但变量var以值方式捕获。
cpp 复制代码
int main() {
    int a = 1, b = 2, c = 3;
    auto lambda_mixed = [=, &c] { // a, b 以值捕获,c 以引用捕获
        std::cout << "a: " << a << ", b: " << b << std::endl;
        c++; // 修改外部的 c
        // a++; // 错误:值捕获的变量是 const 的
    };
    lambda_mixed();
    std::cout << "c: " << c << std::endl;
    return 0;
}

输出:

复制代码
a: 1, b: 2
c: 4
e) 捕获 this 指针 [this][=]

在类的成员函数中,Lambda可以通过捕获this指针来访问类的成员变量和函数。

cpp 复制代码
class MyClass {
public:
    int value = 42;
    void print() {
        // 捕获 this,从而可以访问成员 value
        auto lambda = [this] {
            std::cout << "Member value: " << value << std::endl; // 实际上是 this->value
        };
        lambda();
    }
};

int main() {
    MyClass obj;
    obj.print();
    return 0;
}

输出:

复制代码
Member value: 42

注意 :在C++11中,[=]也会隐式捕获this指针。从C++20开始,[=, this]被标记为重复捕获而不再合法,建议直接使用 [this][=]


4. 参数列表 (parameters)

和普通函数的参数列表几乎一样。可以为空(),也可以定义参数。

cpp 复制代码
#include <iostream>
#include <string>

int main() {
    // 带参数的Lambda
    auto greet = [](const std::string& name) {
        std::cout << "Hello, " << name << "!" << std::endl;
    };
    greet("Alice"); // 调用

    // 带多个参数,用于算法
    auto add = [](int a, int b) -> int {
        return a + b;
    };
    std::cout << "5 + 3 = " << add(5, 3) << std::endl;

    return 0;
}

5. 可变规范 (mutable) mutable

默认情况下,对于值捕获 的变量,Lambda将其视为const值,你无法在函数体内修改它们。如果你需要修改这些副本(注意:修改的是副本,不影响外部变量),就需要使用 mutable 关键字。

cpp 复制代码
int main() {
    int x = 10;
    auto lambda = [x]() mutable { // 使用 mutable
        x++; // 现在可以修改值捕获的副本了
        std::cout << "Inside mutable lambda: " << x << std::endl;
    };
    lambda();
    lambda(); // 每次调用,其内部捕获的副本都会保留修改后的状态
    std::cout << "Outside lambda: " << x << std::endl; // 外部的 x 从未被改变
    return 0;
}

输出:

复制代码
Inside mutable lambda: 11
Inside mutable lambda: 12
Outside lambda: 10

重要mutable 允许你修改的是Lambda自身捕获的副本 ,而不是外部变量。并且它使得Lambda调用运算符 () 不再是 const 的。

这个东西说实在的,几乎没用。


6. 返回类型 -> return-type

大多数情况下,编译器可以推断出Lambda的返回类型。

  • 如果函数体只包含一个return语句,返回类型就是该表达式的类型。
  • 如果函数体包含多个返回语句,且类型不同,或者形式复杂,编译器无法推断,你就必须显式指定返回类型。
cpp 复制代码
int main() {
    // 编译器推断返回类型为 int
    auto inferred = [](int a, int b) { return a + b; };

    // 必须显式指定返回类型为 double
    // 否则多个return语句类型不同(int 和 double),编译器会报错或选择int
    auto explicit_return = [](int a, int b) -> double {
        if (a == 0) {
            return 0; // int
        } else {
            return static_cast<double>(b) / a; // double
        }
    };

    std::cout << inferred(5, 3) << std::endl; // 8
    std::cout << explicit_return(5, 3) << std::endl; // 0.6
    return 0;
}

7. 函数体 { ... }

和普通函数的函数体没有任何区别,可以包含任何有效的C++语句。


8. Lambda的实际应用场景

a) 与STL算法结合(最常用)

这是Lambda的"主场",它让STL算法的调用变得异常简洁。

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> // for std::for_each, std::sort, std::find_if

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

    // 1. 使用 Lambda 进行排序(自定义比较器)
    std::sort(numbers.begin(), numbers.end(),
              [](int a, int b) { return a > b; }); // 降序排列
    // numbers 现在是 {5, 4, 3, 2, 1}

    // 2. 使用 Lambda 遍历元素 (替代传统的函数对象)
    std::for_each(numbers.begin(), numbers.end(),
                  [](int n) { std::cout << n << " "; });
    std::cout << std::endl;

    // 3. 使用 Lambda 查找条件元素
    int threshold = 3;
    auto it = std::find_if(numbers.begin(), numbers.end(),
                           [threshold](int n) { return n < threshold; });
    if (it != numbers.end()) {
        std::cout << "First number less than " << threshold << " is " << *it << std::endl;
    }

    return 0;
}
b) 作为回调函数

异步编程、事件驱动编程中,Lambda非常适合作为一次性回调。

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

// 模拟一个异步操作,完成后调用回调函数
void asyncOperation(std::function<void(int)> callback) {
    std::cout << "Operation started..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    int result = 42;
    std::cout << "Operation finished. Calling callback." << std::endl;
    callback(result); // 调用传入的Lambda
}

int main() {
    // 就地定义一个Lambda作为回调传给异步函数
    asyncOperation([](int result) {
        std::cout << "Callback received result: " << result << std::endl;
    });

    // 主线程等待一下,确保异步操作完成(实际中会用更复杂的方式同步)
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 0;
}
c) 封装局部状态(闭包)

Lambda可以捕获并持有局部状态,形成一个闭包(Closure)。这在C++11之前需要手动编写一个仿函数类才能实现。

cpp 复制代码
#include <iostream>

// 创建一个计数器生成器函数
std::function<int()> makeCounter(int start = 0) {
    // 返回一个Lambda,它捕获了局部变量 `count` 的状态
    return [=]() mutable { // 值捕获 start,mutable 允许修改副本
        return start++;
    };
}

int main() {
    auto counter1 = makeCounter(10);
    auto counter2 = makeCounter();

    std::cout << counter1() << std::endl; // 10
    std::cout << counter1() << std::endl; // 11
    std::cout << counter2() << std::endl; // 0
    std::cout << counter1() << std::endl; // 12
    std::cout << counter2() << std::endl; // 1

    return 0;
}

解释 :每次调用makeCounter都会创建一个新的、独立的Lambda对象,每个对象都持有自己捕获的start变量的副本。调用这个Lambda就会修改并返回它自己内部的那个副本。这就是闭包的行为。


9. Lambda的本质

重要知识点 :Lambda表达式并不是黑魔法。编译器在背后会为你自动生成一个独一无二的匿名仿函数类(Functor Class)

你的Lambda:

cpp 复制代码
int x = 10;
auto lambda = [x](int y) mutable -> int { return x + y; };

会被编译器转换成类似这样的代码:

cpp 复制代码
// 编译器生成的匿名类
class __Some_Unique_Compiler_Generated_Name__ {
private:
    int x; // 值捕获的变量变成了成员变量

public:
    __Some_Unique_Compiler_Generated_Name__(int captured_x) : x(captured_x) {} // 构造函数

    // 重载函数调用运算符 operator()
    int operator()(int y) const { // 注意:如果没有 mutable,这里是 const
        return x + y;
    }
    // 如果有 mutable, operator() 就不是 const 的
};

// 你的 `auto lambda = ...` 这一行实际上相当于:
__Some_Unique_Compiler_Generated_Name__ lambda(x);

理解这一点非常重要,它解释了:

  1. 为什么Lambda可以有状态:因为它的状态保存在其类的成员变量中。
  2. 为什么值捕获的变量默认是const :因为生成的 operator() 默认是 const 成员函数。
  3. 为什么需要 mutablemutable 去掉了生成的 operator()const 限定符。
  4. Lambda的大小:Lambda的大小取决于它捕获的变量的大小。
  5. Lambda如何传递:和传递类对象一样(值传递、引用传递等)。

总结

特性 语法 说明
捕获列表 [x, &y] 定义如何访问外部变量
参数列表 (int a, double b) 和普通函数一样
可变规范 mutable 允许修改值捕获的变量副本
返回类型 -> double 可省略(编译器推断),复杂时需显式指定
函数体 { ... } 包含要执行的代码
相关推荐
yangpipi-6 小时前
C++并发编程-23. 线程间切分任务的方法
开发语言·c++
爬虫程序猿7 小时前
利用 Java 爬虫获取淘宝商品 SKU 详细信息实战指南
java·开发语言·爬虫
F2E_Zhangmo7 小时前
基于cornerstone3D的dicom影像浏览器 第五章 在Displayer四个角落显示信息
开发语言·前端·javascript
楼田莉子7 小时前
C++算法专题学习——分治
数据结构·c++·学习·算法·leetcode·排序算法
He1955018 小时前
Go初级之十:错误处理与程序健壮性
开发语言·python·golang
m0_738120728 小时前
CTFshow系列——PHP特性Web93-96
开发语言·安全·web安全·php·ctfshow
ulias2128 小时前
各种背包问题简述
数据结构·c++·算法·动态规划