Menu
CoddyTech

深入了解 Lambda 表达式

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 85 课,共 104 课。

Lambda 表达式在 C++11 中引入,是你可以在行内定义的匿名函数。你已经见过与 STL 算法搭配使用的基础 Lambda 表达式,理解它们的完整语法还能解锁强大的功能,让你捕获变量并控制访问方式。

完整的 lambda 语法是:[capture](parameters) mutable -> return_type { body }。捕获子句决定 lambda 可以访问哪些外部变量,以及如何访问:

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    
    auto byValue = [x]() { return x * 2; };        // 复制 x
    auto byRef = [&y]() { y += 5; };               // 引用 y
    auto allByValue = [=]() { return x + y; };     // 全部复制
    auto allByRef = [&]() { x++; y++; };           // 全部引用
    auto mixed = [x, &y]() { y += x; };            // 混合使用两种方式
    
    byRef();
    std::cout << y << "\n";  // 25
}

默认情况下,按值捕获的变量在 lambda 内部是 const。mutable 关键字允许修改这些副本:

int counter = 0;
auto increment = [counter]() mutable {
    return ++counter;  // 修改 lambda 中的副本
};

std::cout << increment() << "\n";  // 1
std::cout << increment() << "\n";  // 2
std::cout << counter << "\n";      // 0,原始值不变

C++14 添加了初始化捕获,让你可以创建新变量,或将对象移动到 lambda 中:

auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
    return *p;
};  // 所有权已转移到 lambda 中

在 OOP 中,Lambda 特别有用。当你需要将行为作为参数传递时,例如用于回调、自定义比较器或事件处理程序,就不必单独定义函数对象。

challenge icon

挑战

简单

我们来构建一个事件处理系统,展示不同捕获模式下 lambda 表达式的强大之处。你将创建一个简单的事件分发器,用于存储和调用回调,演示 lambda 如何以各种方式捕获外部状态。

你需要把代码分成 3 个文件:

  • EventDispatcher.h:定义一个管理事件回调的 EventDispatcher 类。

    分发器应使用 std::vector 和 std::function<void()> 来存储回调。添加以下方法:

    • addCallback(std::function<void()> callback):向列表中添加一个回调
    • fireAll():按顺序调用所有已存储的回调
    • clear():移除所有回调

    你需要包含 <functional> 和 <vector>。

  • EventDispatcher.cpp:实现分发器的方法。fireAll() 方法只需遍历所有回调并逐个调用。
  • main.cpp:读取两个输入:
    1. 一个基数(整数)
    2. 一个乘数(整数)

    创建一个 EventDispatcher,并通过添加 3 个回调来演示不同的 lambda 捕获方式:

    1. 一个按值捕获基数并打印 Base value: [base] 的 lambda
    2. 一个按引用捕获乘数、将其加 1,然后打印 Multiplier after increment: [multiplier] 的 lambda
    3. 一个按值捕获计数器变量(初始值为 0)、每次调用时都将其加 1,并打印 Call count: [counter] 的 mutable lambda

    添加所有回调后,调用两次 fireAll(),看看不同的捕获模式在多次调用时会如何表现。在两次调用 fireAll() 之间,打印 --- 作为分隔符。

    最后,在两轮调用结束后,从 main 打印乘数变量的最终值,以展示按引用捕获对它的影响:Final multiplier: [multiplier]

例如,输入 10 和 5 时:

Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 1
Final multiplier: 7

输入 42 和 0 时:

Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 1
Final multiplier: 2

注意以下关键行为:按值捕获会保留原始基数,不会改变它;按引用捕获会修改 main 中实际的乘数变量,并在多次调用间累积;mutable lambda 中的计数器每轮都会读取到 1。这是因为 fireAll() 会按值遍历回调:每一轮都会调用存储的 std::function 的一个新副本。这个副本捕获的计数器初始值为 0,并递增到 1,因此存储的回调自身状态始终不会递增。

亲手试试

#include <iostream>
#include "EventDispatcher.h"

using namespace std;

int main() {
    int base;
    int multiplier;
    cin >> base;
    cin >> multiplier;
    
    EventDispatcher dispatcher;
    
    // TODO:添加一个按值捕获 base 的 lambda
    // 它应输出:"Base value: [base]"
    
    // TODO:添加一个按引用捕获 multiplier 的 lambda
    // 它应将 multiplier 加 1,然后输出:"Multiplier after increment: [multiplier]"
    
    // TODO:添加一个可修改的 lambda,按值捕获一个初始化为 0 的计数器
    // 它应将计数器加 1,然后输出:"Call count: [counter]"
    
    // TODO:调用 fireAll() 来执行所有回调
    
    // TODO:输出 "---" 作为分隔符
    
    // TODO:再次调用 fireAll()
    
    // TODO:输出 multiplier 的最终值:"Final multiplier: [multiplier]"
    
    return 0;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器