C++笔记:Lambda表达式

C++11:一切的开始

在 C++11 中,Lambda 的基本语法被确定: [capture](parameters) mutable -> return_type { body }

  • 捕获(Capture): 只能捕获明确类型的变量。

    • [=]:值捕获所有外部变量。

    • [&]:引用捕获所有外部变量。

    • [x, &y]:特定捕获。

  • 参数类型: 必须明确指定,不支持泛型。

  • 痛点: 无法在捕获列表中初始化新变量(比如移动一个 std::unique_ptr),参数类型太死板。

1.深度解析 mutable 的含义

结论先行: mutable 的作用是取消 Lambda 内部对"值捕获变量"的只读限制。

当你写下一个 Lambda 时,编译器实际上会为你生成一个匿名的闭包类(Closure Class)

  • 默认情况下,这个闭包类的 operator()const 的。

  • 这意味着在 Lambda 函数体内部,你不能修改那些通过值捕获进来的变量。

为什么需要它?

如果你尝试在不加 mutable 的情况下修改值捕获的变量,编译器会报错:

cpp 复制代码
int x = 10;
auto f = [x]() { 
    x++; // 错误!x 是只读的
};

加上 mutable 后,编译器生成的 operator() 不再有 const 属性:

cpp 复制代码
auto f = [x]() mutable { 
    x++; // 编译通过
    std::cout << "Inside: " << x << std::endl;
};

f(); // 输出 11
std::cout << "Outside: " << x << std::endl; // 输出 10

2. 值捕获的变量可以修改吗?

这里有一个非常重要的区分:修改的是谁?

  1. 在 Lambda 内部: 默认不可以修改。加上 mutable 后可以修改,但修改的是闭包内部拷贝的那份副本

  2. 在 Lambda 外部: 无论内部怎么改,外部原始变量绝对不会变

因为"值捕获"在 Lambda 定义的那一刻,就发生了一次拷贝构造 。内部的 x 和外部的 x 已经是两个独立的内存地址了。


3. 特定捕获(Explicit Capture)的写法

特定捕获(也叫显式捕获)是指不使用 [=][&] 这种"全家桶"模式,而是精确指定要捕获哪个变量。

如何区分值捕获与引用捕获?

区分的唯一标识符就是 & 符号

写法 捕获方式 含义
[x] 值捕获 拷贝 x 的值到 Lambda 内部。内部修改不影响外部。
[&x] 引用捕获 内部直接操作外部 x 的引用。内部修改同步影响外部。
[x, &y] 混合捕获 x 按值捕获,y 按引用捕获。
[this] 指针捕获 捕获当前类实例的指针,允许访问成员变量和函数。

语法规则对比示例:

cpp 复制代码
int a = 1, b = 1;

auto demo = [a, &b]() mutable {
    a++; // 合法,因为有 mutable。但外部 a 仍为 1。
    b++; // 合法。外部 b 变为 2。
};

4. 捕获列表的"组合拳"

C++11 允许你设置一个"默认模式",然后对特定变量进行例外处理:

  • [=, &x] :默认全部按值捕获,但 x 破例按引用捕获。

  • [&, x] :默认全部按引用捕获,但 x 破例按值捕获。

注意事项:

  • 重复捕获是禁止的: 例如 [&, &x] 是错误的,因为 & 已经表示默认引用捕获了,再单独写 &x 就多此一举,编译器会报错。

  • 生命周期安全: 引用捕获 [&] 非常危险。如果 Lambda 被保存并在原始变量销毁后才执行(比如在多线程或回调中),会导致悬挂引用(Dangling Reference),引发崩溃。

C++14:初始化捕获与泛型

C++14 是 Lambda 的第一次重大飞跃,解决了"灵活度"问题。

1. 广义捕获(Generalized Lambda Capture / Init-capture)

在 C++11 中,你只能捕获作用域内已经存在的变量。而 C++14 允许你在捕获列表中创建一个新变量,并对其进行初始化。

语法

[新变量名 = 表达式](parameters) { ... }

核心价值:支持"移动语义"

这是 C++14 广义捕获最经典的应用。在 C++11 中,你无法将一个 std::unique_ptr 捕获进 Lambda,因为 unique_ptr 不能拷贝。 C++14 解决了这个问题:

cpp 复制代码
auto ptr = std::make_unique<int>(42);

// 使用 std::move 将外部变量移动到 Lambda 内部的新变量 p 中
auto lambda = [p = std::move(ptr)]() {
    std::cout << "The value is: " << *p << std::endl;
};

// 此时原 ptr 已经为空

核心价值:重命名或计算

你可以在捕获时重命名变量,或者直接在捕获列表中进行计算:

cpp 复制代码
int x = 10;
auto f = [y = x + 1, z = 100]() {
    return y + z; // y 是 11, z 是 100
};

本质: 编译器在生成的闭包类中,直接用你写的表达式来初始化成员变量。


2. 泛型 Lambda(Generic Lambdas)

在 C++11 中,Lambda 的参数类型必须是确定的(比如 intstring)。如果你想写一个能处理多种类型的 Lambda,只能写多个或者去写传统的函数模板。

语法

C++14 允许在参数列表中使用 auto

cpp 复制代码
auto sum = [](auto a, auto b) {
    return a + b;
};

std::cout << sum(1, 2);       // 推导为 int
std::cout << sum(1.5, 2.5);   // 推导为 double
std::cout << sum(std::string("A"), "B"); // 推导为 std::string

本质:模板 operator()

当你写出 auto 参数时,编译器实际上为闭包类生成了一个成员函数模板

cpp 复制代码
// 编译器生成的闭包类大致如下:
struct UnnamedLambda {
    template<typename T1, typename T2>
    auto operator()(T1 a, T2 b) const {
        return a + b;
    }
};

3. 为什么这两者结合很强大?

泛型 Lambda 和广义捕获结合,可以写出非常灵活的代码。比如,我们可以捕获一个"工厂对象"并处理不同类型的参数:

cpp 复制代码
auto factory = [](auto type_info) {
    // 这里的 type_info 可以是任意类型
    return [data = std::make_unique<Data>(type_info)](auto extra) {
        // 既利用了广义捕获的移动语义,又利用了泛型参数
        data->do_something(extra);
    };
};

总结 C++14 的改进

  1. 解决了"动不了"的问题 :支持 std::move 捕获。

  2. 解决了"写不死"的问题 :参数支持 auto,实现了一次编写,多类型适用。

  3. 语法灵活性:捕获列表不再仅仅是"取变量",而是一个可以进行"赋值操作"的地方。

auto&&

1. 为什么要用 auto&&

如果只写 auto a,参数是按值传递 的,会发生拷贝,这对于大型对象(如 std::vector)性能很差,且无法处理 std::unique_ptr 这种不可拷贝的对象。

如果使用 auto&&,它就变成了万能引用

  • 如果传入左值,它推导为左值引用。

  • 如果传入右值,它推导为右值引用。

这样既能避免拷贝,又能保持参数的原始属性。


2. 配合 decltype 实现完美转发

这是 C++14 中最硬核的写法。因为 Lambda 没有显式的模板参数名(比如 T),我们无法直接写 std::forward<T>(a)。这时候必须借助 decltype

cpp 复制代码
auto wrapper = [](auto&& arg) {
    // 使用 decltype(arg) 抓取参数类型,实现完美转发
    do_something(std::forward<decltype(arg)>(arg));
};

原理简述:

  • arg 是左值时,decltype(arg) 是左值引用,std::forward 按左值处理。

  • arg 是右值时,decltype(arg) 是右值引用,std::forward 按右值处理。


3. C++20 的拯救:显式模板参数

由于 auto&& 的复杂性,这正是 C++20 改进的动力。C++20 觉得用 decltype 太绕了,直接允许你在 Lambda 上写模板参数列表:

cpp 复制代码
// C++20 写法:清爽、直观
auto wrapper = []<typename T>(T&& arg) {
    do_something(std::forward<T>(arg));
};

这样我们就回到了熟悉的 std::forward<T>,不再需要去推导 decltype 了。

C++17:更加完善

C++17 侧重于解决类成员捕获和编译时计算的问题。

*this 捕获

在 C++11/14 中,如果你在类成员函数里写 [this],捕获的是指针。如果对象在 Lambda 执行前析构了,就会崩溃。C++17 引入了 [*this],直接按值拷贝一份当前对象,保证了安全性。

但是在很多异步框架(如 Boost.Asio)中,我们习惯通过捕获 shared_ptr 来延长对象的生命周期。但 C++17 引入 [*this] 捕获主要基于以下三个原因:

和捕获共享指针的区别?

A. 性能开销

shared_ptr 的引用计数是原子操作 ,且涉及额外的控制块(Control Block)访问。如果你的对象很小,或者你只是想在局部范围内安全地使用一份快照,直接按值拷贝对象[*this])通常比维护一套引用计数系统更快。

B. 并不是所有对象都由 shared_ptr 管理

这是最根本的原因。很多对象是分配在上的,或者是作为其他类的成员变量存在的。

  • 如果对象不是用 make_shared 创建的,调用 shared_from_this() 会直接抛出异常(std::bad_weak_ptr)。

  • [*this] 提供了一种通用方案,无论对象在堆上还是栈上,它都能通过拷贝一份副本到 Lambda 闭包中,确保 Lambda 执行时数据的有效性。

C. 语义差异:延长生命周期 vs 获取快照
  • shared_ptr :你是想让 Lambda 共享原对象。如果原对象在外部被修改了,Lambda 内部看到的也会变。

  • [*this] :你是想让 Lambda 拥有原对象的一个"瞬间快照"。即便外部原对象被改得面目全非,Lambda 里的副本依然保持捕获那一刻的状态。

和值捕获的区别?

直觉上,[*this] 确实像是 [self = *this] 的语法糖,但实际上,它不仅解决了语法便利性 ,更在语义严谨性编译器行为上补齐了最后一块拼图。

我们可以从以下三个维度来拆解它们的区别:

A. 语法便利性:消灭冗余

在 C++14 中,如果你想拷贝当前对象,你必须手动给它起个名字:

cpp 复制代码
// C++14 的"模仿"写法
auto lambda = [self = *this]() { 
    self.do_something(); 
};

痛点:

  • 你必须显式通过 self. 来访问成员,不能直接写成员变量名。

  • 如果你习惯了直接写变量名(隐式通过 this->),那么在 C++14 里你需要把所有代码重构一遍。

C++17 的 [*this]

cpp 复制代码
auto lambda = [*this]() { 
    do_something(); // 依然可以隐式使用 this 指针,但这个 this 指向的是副本
};

它允许你保持原有的代码书写习惯,同时在底层帮你完成了对象的拷贝。


B. 语义逻辑:捕获的是"对象"而非"变量"

这是最核心的区别。在 C++ 的 Lambda 设计哲学中,捕获列表 [] 的目的是建立闭包与外部环境的联系

  • C++14 的 [self = *this] :这被视为一种初始化捕获(Init-capture) 。编译器认为你定义了一个名为 self 的新局部变量。在 Lambda 的作用域内,this 指针依然是指向原来的外部对象。

  • C++17 的 [*this] :这被视为一种特殊的当前对象捕获 。它改变了 Lambda 内部 this 的含义。在 Lambda 的函数体内,this 不再指向外部的那个对象,而是指向闭包内部拷贝出来的那个副本

这意味着,使用 [*this] 时,闭包内部的成员函数调用、成员变量访问,全都自动切换到了副本上,而不需要你手动写 self.


C. 编译器生成的闭包结构

我们可以对比一下编译器生成的伪代码(简化版):

C++14 [self = *this]
cpp 复制代码
struct Closure {
    Widget self; // 手动定义的成员
    void operator()() const {
        self.value; // 必须通过 self 访问
    }
};
C++17 [*this]
cpp 复制代码
struct Closure {
    Widget __this_copy; // 编译器自动生成的副本
    void operator()() const {
        // 编译器在内部偷偷把所有对 Widget 成员的访问
        // 映射到了 __this_copy 上
        __this_copy.value; 
    }
};

D. 为什么不直接用 C++14 的写法?(实战陷阱)

如果你在 C++14 中混用 [self = *this] 和其他隐式捕获,很容易写出 Bug:

cpp 复制代码
// 危险的 C++14 混合写法
auto lambda = [self = *this, =]() {
    self.foo();  // 访问副本
    bar();       // 警告!bar() 实际上是 this->bar(),依然指向原对象!
};

这种"分裂"的语义(一部分访问副本,一部分访问原指针)是导致代码不安全的重要原因。

C++17 的 [*this] 统一了战场 :一旦声明了 [*this],Lambda 内部所有的成员访问行为都变得一致且安全,全部指向副本。

constexpr Lambda

在 C++17 之前,Lambda 只能在运行时执行。C++17 规定:只要 Lambda 满足 constexpr 函数的条件,它默认就是 constexpr 的。

语法体现

你可以显式加上 constexpr 关键字,也可以不加。

cpp 复制代码
// 显式声明
auto squared = [](int n) constexpr {
    return n * n;
};

// 在编译期使用
int arr[squared(3)]; // 定义一个长度为 9 的数组

为什么这很有用?

  1. 消除魔术数字:以前我们需要用复杂的模板元编程或静态常量来计算编译期数值,现在写个 Lambda 就行。

  2. 更强的性能优化:编译器能更早地看到计算逻辑并进行常量折叠。

C++20:终极形态

C++20 让 Lambda 变得极其强大,几乎可以替代所有普通函数。

显式模板参数

虽然 C++14 有了 auto,但有时我们需要限制两个参数类型必须相同,或者需要获取模板类型 T。

cpp 复制代码
// 只有 C++20 能简洁地写出:
auto func = []<typename T>(std::vector<T> const& vec) {
    T value; // 可以直接使用类型 T
};

完善的捕获与约束

  • Concepts 支持: 参数可以配合 requires 或 Concept 进行约束。

    cpp 复制代码
    auto add = [](std::integral auto a, std::integral auto b) { return a + b; };
  • 弃用 [=] 隐式捕获 this C++20 开始,如果你写 [=] 却访问了类成员,编译器会警告。你必须显式写出 [=, this]

  • 无状态 Lambda 可默认构造: 如果 Lambda 没捕获任何东西,它现在可以被 decltype 后直接实例化。

核心变化总结表

特性 C++11 C++14 C++17 C++20
参数类型 固定类型 auto (泛型) 保持泛型 模板参数列表 <T>
捕获写法 基础值/引用 初始化捕获 (可移动) [*this] (拷贝对象) 显式 this 捕获要求
编译期 运行时为主 运行时 constexpr consteval
约束 Concepts (requires)
相关推荐
问心无愧05131 小时前
ctf show web 入门38
笔记
程序猿乐锅1 小时前
【Tilas|第六篇】班级管理实现
java·笔记·tlias
minji...1 小时前
Linux 网络基础(五)守护进程化,前后台进程组,作业,会话,setsid(),daemon(),端口号频繁更换问题
linux·运维·服务器·网络·c++·tcp/ip
AOwhisky1 小时前
Docker 学习笔记:从生态系统到镜像构建
linux·运维·笔记·学习·docker·容器
Brilliantwxx1 小时前
【算法题】递归树+哈希表+分治异或+双指针
开发语言·c++·笔记·算法
Hello:CodeWorld1 小时前
高性能多线程数据采集与持久化方案设计与实现
开发语言·c++
程序猿编码1 小时前
Linux 高负载场景下 Web 服务访问日志极速定位工具实现解析(C/C++代码实现)
linux·服务器·c语言·前端·c++
无限进步_1 小时前
【C++】智能指针族谱:auto_ptr、unique_ptr、shared_ptr
java·开发语言·数据结构·c++·算法
LuminousCPP1 小时前
C 语言文件操作全攻略:从基础读写到随机访问与缓冲区原理
c语言·经验分享·笔记·文件操作