Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

现代 C++ / Modern C++

1. 现代 C++ 总览:不是特性清单,而是表达能力 / Modern C++ Overview

2. 移动、转发与值类别:std::move 为什么不移动 / Move, Forward, and Value Categories

3. 类型推导、初始化与 constexpr / Type Deduction, Initialization, and constexpr

4. 模板、实例化与 SFINAE / Templates, Instantiation, and SFINAE

5. concept 与约束:把模板要求写在接口上 / Concepts and Constraints

6. 异常、RAII 与 noexcept:失败路径也要清理 / Exceptions, RAII, and noexcept

7. lambda、捕获与生命周期:回调不是免费午餐 / Lambdas, Captures, and Lifetime

8. callable wrapper 与 std::invoke:怎样保存和调用行为 / Callable Wrappers and std::invoke

9. 词汇类型与错误建模:让接口自己说话 / Vocabulary Types and Error Modeling

10. 协程与异步抽象:先理解状态机 / Coroutines and Async Abstractions

本页目录

lambda、捕获与生命周期:回调不是免费午餐 / Lambdas, Captures, and Lifetime ​

lambda 最容易被介绍成“现场写一个小函数”。

这没错,但不够。真正重要的是:lambda 是对象,它可能保存状态,而保存状态就会带来生命周期问题。

1. 没有捕获的 lambda 像小函数 ​

看代码:

cpp
auto add = [](int a, int b) {
    return a + b;
};

int result = add(1, 2);
1
2
3
4
5

这段代码定义了一个可调用对象。

可以先把它理解成:

text
add 是一个对象
这个对象可以像函数一样被调用
1
2

没有捕获时,它不保存外部变量。

所以它比较接近普通函数。

2. 有捕获的 lambda 是闭包对象 ​

看代码:

cpp
int base = 10;

auto add_base = [base](int x) {
    return x + base;
};
1
2
3
4
5

[base] 表示按值捕获 base。

可以近似想成编译器生成了一个类:

cpp
struct Closure {
    int base;

    int operator()(int x) const {
        return x + base;
    }
};
1
2
3
4
5
6
7

真实实现不要求完全这样。

但这个模型能解释两件事:

text
lambda 是对象
捕获的值会成为对象内部状态
1
2

图:

text
add_base closure object
+------------------+
| base copy = 10   |
+------------------+
| operator()(x)    |
+------------------+
1
2
3
4
5
6

3. 按值捕获:保存副本 ​

cpp
int value = 1;

auto f = [value] {
    return value + 1;
};

value = 100;
int result = f(); // 通常还是 2
1
2
3
4
5
6
7
8

因为 lambda 保存的是捕获时的副本。

它不是每次调用都重新读取外部 value。

如果想修改这个副本,需要 mutable:

cpp
auto counter = [value = 0]() mutable {
    return ++value;
};
1
2
3

这里 value = 0 是初始化捕获。

lambda 对象内部有自己的 value。

每次调用修改的是这个内部副本。

4. 按引用捕获:不拥有,只借用 ​

按引用捕获:

cpp
int value = 1;

auto f = [&value] {
    return value + 1;
};
1
2
3
4
5

lambda 内部保存的不是独立副本。

它引用外部对象。

所以外部对象改变,lambda 看到的也会改变。

cpp
value = 100;
int result = f(); // 101
1
2

这很方便,也很危险。

危险来自生命周期:

cpp
#include <functional>

std::function<int()> make_counter() {
    int value = 0;
    return [&value] {
        return ++value;
    };
}
1
2
3
4
5
6
7
8

make_counter() 返回后,局部变量 value 已经销毁。

返回出去的 lambda 还引用它。

text
make_counter frame
+------------------+
| value = 0        |
+------------------+
       ^
       |
lambda stores reference

after return

frame gone
lambda still stores reference to dead object
1
2
3
4
5
6
7
8
9
10
11
12

这就是悬垂引用。

5. 返回 lambda 时优先按值捕获需要的状态 ​

安全写法:

cpp
#include <functional>

std::function<int()> make_counter() {
    return [value = 0]() mutable {
        return ++value;
    };
}
1
2
3
4
5
6
7

这里 value 是 lambda 对象自己的成员。

make_counter() 返回后,lambda 对象仍然拥有这份状态。

这就是“捕获方式”和“闭包对象生命周期”之间的关系。

6. this 捕获尤其要小心 ​

看代码:

cpp
struct Widget {
    int value{42};

    auto callback() {
        return [this] {
            return value;
        };
    }
};
1
2
3
4
5
6
7
8
9

[this] 捕获的是当前对象指针。

如果 Widget 对象先销毁,回调后执行:

text
lambda object
  |
  v
this pointer
  |
  v
destroyed Widget object
1
2
3
4
5
6
7

这也是悬垂。

更安全的设计取决于需求。

如果只需要当前值:

cpp
auto callback() {
    int copy = value;
    return [copy] {
        return copy;
    };
}
1
2
3
4
5
6

如果需要共享对象生命周期,可能要用 shared_ptr/weak_ptr 设计。

但不要默认把 this 捕获丢进异步回调里。

7. 捕获默认值不要滥用 ​

两种默认捕获:

cpp
[=] { }  // 默认按值捕获用到的外部变量
[&] { }  // 默认按引用捕获用到的外部变量
1
2

它们写起来短,但大型代码里可能不清楚到底捕获了什么。

尤其 [&] 放进延迟执行、异步执行、保存到成员变量的回调时,要非常谨慎。

更可读的方式常常是显式列出:

cpp
[count, name] {
    // ...
}
1
2
3

或者:

cpp
[&output] {
    // 明确知道只借用 output
}
1
2
3

8. 泛型 lambda 和模板 ​

现代 C++ 支持:

cpp
auto add = [](auto a, auto b) {
    return a + b;
};
1
2
3

这个 lambda 的调用运算符本质上像模板。

也就是说:

text
add(1, 2)
  实例化一份适合 int 的调用

add(std::string{}, std::string{})
  实例化另一份适合 string 的调用
1
2
3
4
5

如果某个类型不支持 a + b,错误会在调用时出现。

所以泛型 lambda 和模板一样,也要考虑约束和错误信息。

9. lambda 转成函数指针 ​

没有捕获的 lambda 可以转换成函数指针:

cpp
int (*fp)(int, int) = [](int a, int b) {
    return a + b;
};
1
2
3

有捕获的 lambda 不行:

cpp
int base = 10;
// int (*fp)(int) = [base](int x) { return x + base; }; // 错
1
2

原因是有捕获的 lambda 需要保存状态。

普通函数指针只能指向代码入口,不能保存对象状态。

10. 本篇总结 ​

lambda 的核心模型:

text
lambda expression
  |
  v
closure object
  |
  +-- captured state
  |
  +-- operator()
1
2
3
4
5
6
7
8

每次写 lambda 都问:

text
它捕获了什么?
按值还是按引用?
lambda 对象会活多久?
被引用对象会活多久?
是否捕获 this?
是否会被异步执行或保存起来?
1
2
3
4
5
6

lambda 不是危险语法。

真正危险的是:回调活得比它引用的对象更久。

最后更新于:

Pager
上一篇6. 异常、RAII 与 noexcept:失败路径也要清理 / Exceptions, RAII, and noexcept
下一篇8. callable wrapper 与 std::invoke:怎样保存和调用行为 / Callable Wrappers and std::invoke

持续记录,持续成长

Copyright © Tidenflow