CppCon 2020 学习:How C++20 changes the way we write code
这段代码逐层迭代二维 std::vector,意思是:
#include <vector>
#include <iostream>
int main() {
std::vector<std::vector<int>> vec = {{1, 0}, {0, 1}};
// 创建一个二维向量,相当于:
// vec[0] = {1, 0}
// vec[1] = {0, 1}
for (std::vector<std::vector<int>>::iterator i = vec.begin(); i != vec.end(); ++i) {
// 外层循环:i 是一个迭代器,指向 vec 中的每一行(std::vector<int>)
for (std::vector<int>::const_iterator j = i->begin(); j != i->end(); ++j) {
// 内层循环:j 是一个迭代器,指向当前行里的每个元素(int)
/* do something */
// 在这里对 *j 进行处理
std::cout << *j << " ";
}
}
}
解释
- 外层循环:遍历二维向量的每一行(即
std::vector<int>)。 - 内层循环:遍历当前行中的每一个
int元素。 i是非常量迭代器,所以外层可以修改行(例如增加或删除元素)。j是常量迭代器,表示内层只读取数据,不会修改它。
如果你想用现代 C++ 的写法,可以改成更简洁的范围 for 循环:
#include <vector>
#include <iostream>
int main() {
std::vector<std::vector<int>> vec = {{1, 0}, {0, 1}};
// 创建一个二维向量,相当于:
// vec[0] = {1, 0}
// vec[1] = {0, 1}
for (auto& row : vec) {
for (const auto& value : row) {
// 内层循环:j 是一个迭代器,指向当前行里的每个元素(int)
/* do something */
// 在这里对 *j 进行处理
std::cout << value << " ";
}
}
}
这种写法更易读,而且不用写长长的迭代器类型。
如果你愿意,我可以帮你画一个二维迭代器指针移动示意图,让这段代码更直观。这样能更快理解。
这段代码的作用是:自定义一个比较器 sorter_type,然后用它来对 vec(一个 std::vector<std::vector<int>>)进行排序。
逐行解释
struct sorter_type {
// 自定义排序规则
bool operator()(const std::vector<int>& lhs, const std::vector<int>& rhs) {
return /* something */; // 这里写排序条件,返回 true 表示 lhs 排在 rhs 前面
}
} sorter;
struct sorter_type定义了一个函数对象(仿函数)。operator()是调用运算符重载,允许这个结构体的对象像函数一样被调用。lhs和rhs分别表示比较的两个元素,它们是std::vector<int>类型(即二维 vector 的一行)。/* something */是排序条件,例如:- 按第一列升序:
lhs[0] < rhs[0] - 按行内元素和升序:
std::accumulate(lhs.begin(), lhs.end(), 0) < std::accumulate(rhs.begin(), rhs.end(), 0)
- 按第一列升序:
std::sort(vec.begin(), vec.end(), sorter);
std::sort是 C++ 标准库的快速排序函数(实际上通常是 introsort)。vec.begin()和vec.end()表示对vec中的所有行进行排序。sorter是前面定义的比较器对象,它决定了排序顺序。
举个例子
假设:
std::vector<std::vector<int>> vec = {{2, 5}, {1, 9}, {3, 4}};
如果比较器是:
bool operator()(const std::vector<int>& lhs, const std::vector<int>& rhs) {
return lhs[0] < rhs[0]; // 按第一列升序
}
排序结果就是:
{ {1, 9}, {2, 5}, {3, 4} }
如果你愿意,我可以帮你写一个**同时支持多列排序(类似 Excel)**的比较器,让 /* something */ 变得更强大。这样二维向量就可以按“第一列优先、第二列次之”这种方式排序。
一个完整可运行的 C++ 例子,演示如何用自定义比较器对二维 std::vector 排序,并且支持多列排序(类似 Excel 的“第一列优先、第二列次之”规则)。
#include <iostream>
#include <vector>
#include <algorithm> // std::sort
// 定义比较器(仿函数)
struct sorter_type {
bool operator()(const std::vector<int>& lhs, const std::vector<int>& rhs) const {
// 第一列优先排序
if (lhs[0] != rhs[0]) {
return lhs[0] < rhs[0]; // 第一列升序
}
// 如果第一列相等,则按第二列排序
return lhs[1] < rhs[1]; // 第二列升序
}
};
int main() {
// 二维向量
std::vector<std::vector<int>> vec = {{2, 5}, {1, 9}, {3, 4}, {1, 2}, {2, 1}};
std::cout << "排序前:\n";
for (auto& row : vec) {
std::cout << row[0] << " " << row[1] << "\n";
}
// 排序
std::sort(vec.begin(), vec.end(), sorter_type());
std::cout << "\n排序后(按第一列升序,第一列相等时按第二列升序):\n";
for (auto& row : vec) {
std::cout << row[0] << " " << row[1] << "\n";
}
return 0;
}
输出结果
排序前:
2 5
1 9
3 4
1 2
2 1
排序后(按第一列升序,第一列相等时按第二列升序):
1 2
1 9
2 1
2 5
3 4
关键点
sorter_type是一个仿函数(函数对象),实现了operator(),可以像函数一样调用。- 先比较第一列,如果不同则直接决定顺序;如果相同,再比较第二列。
std::sort会根据比较器返回的true/false来调整元素位置。
我还可以帮你写一个支持任意列优先级顺序的比较器,这样你不用每次手写 if 判断。
按顺序梳理一下这些幻灯片里的例子,然后结合最后一页的 call / yield / resume / return 做解释。
1. 普通函数调用流程
auto f() {
int x = 0;
// some code
return x;
}
- call:调用函数
f()。 - return:函数一次性执行到结尾,返回结果
x。 - 特点:函数执行是不可中断的,调用一次就执行完毕退出。
2. 静态变量实现“记忆”功能
int f() {
static int i = 0;
return i++;
}
- 每次调用
f()都会在上一次的基础上递增i。 - 这里
static int i存储在全局静态区,所以函数的多次调用共享这个状态。 - 缺点是:这是全局状态,会带来可维护性问题。
3. 类的状态封装
class my_generator {
int i = 0;
public:
int operator()() {
return i++;
}
};
- 用对象成员变量
i存储状态,这样多个生成器实例互不干扰。 - 本质上是手动实现了一个状态机。
4. Lambda 捕获状态
auto g = [i = 0]() mutable {
return i++;
};
- Lambda 捕获
i并且用mutable允许修改它。 - 这样
g()每次调用都会延续上次的i值。
5. 进入协程思想
普通函数是:
call → run to completion → return
协程允许:
call → run until yield → 暂停(保存状态)
→ resume(从暂停位置继续) → ...
→ 最终 return
也就是:
- yield:把一个中间值返回给调用方,但保留函数内部的执行状态。
- resume:下次调用时,从
yield停下的位置继续执行。 - return:最终结束并返回最终值。
6. 对比
- 普通函数:一次性执行完毕 → 返回 → 状态丢失。
- 静态变量 / 类 / Lambda:通过变量手动保存状态。
- 协程:编译器帮你自动保存和恢复状态,让“暂停-继续”变得自然。
如果你愿意,我可以帮你把最后的
call → yield → resume → return
成一个可运行的 C++20 协程示例,让这个流程直观跑出来看效果。
标题是 “Design of a Separable Transition-Diagram Compiler”,介绍了一种可分离、可高效运行的 COBOL 编译器设计方法。
背景
- 当时的计算机(如 Burroughs 220)内存很小(几千字),磁带为主要存储介质。
- COBOL 编译器通常很大,需要多次扫描(多 pass),而且被认为很复杂。
- 这篇论文提出了一个快速、一遍(one-pass)的语法驱动编译器设计,可以在中等规模计算机上高效运行。
主要观点(反驳三种当时的主流看法)
- 语法驱动编译器速度慢 → 他证明可以很快(>100 张源代码卡片/分钟)。
- 编译器必须用编译器写 → 他展示了可以用其他方式构建高效编译器。
- COBOL 编译器必须很复杂 → 他的设计紧凑且易于移植。
核心技术
- 可分离(Separable)程序组织
- 一个程序是可分离的,如果它被拆成多个处理模块(processing segments),这些模块可以分别存储、加载、运行,而不需要一次性全部载入。
- 这样可以适配不同内存大小的机器:
- 大内存 → 所有模块驻留内存。
- 小内存 → 模块可存储在磁带/外存,需要时加载。
- 协程方法(Coroutine method)
- 用协程将不同的编译阶段解耦,例如词法分析器和语法分析器互为生产者/消费者,通过协程切换而不是函数调用传递数据。
- 这样可以避免复杂的状态机回溯,提高速度。
- 状态转移图(Transition Diagrams)
- 用状态机图直接描述语法规则,让语法分析器高效且结构清晰。
实际效果
- 在 5000 字内存的 Burroughs 220 上测试:
- 语法检查速度可达 每分钟 140 张穿孔卡。
- 估算在 10000 字内存的机器上可实现完整的一遍编译,速度 超过每分钟 100 张卡。
小结
- 这个设计的亮点是:
- 可分离结构(适配不同硬件)
- 协程驱动的阶段切换(高效的数据流)
- 用状态图直接做语法分析(直观、易实现)
- 它不止适用于 COBOL,也适合 ALGOL 等其他语言编译器。
如果你愿意,我可以帮你画一张 “协程 + 可分离编译器” 工作原理图,把它在 1963 年是怎么运作的直观画出来,方便理解它和现代协程编译器的联系。
这个例子看起来像是在描述一个生成器(generator)函数的行为,但这里的 yield 在 C++ 里并不是标准关键字(这是更像 Python、C#、或者 C++20 协程里的语法)。
代码理解(按生成器语义解释)
int f() {
int i = 0;
while (true)
yield i++; // 每次调用返回当前 i,然后 i 自增
}
int main() {
std::cout << f() << '\n'; // 第一次调用 f(),返回 0
std::cout << f() << '\n'; // 第二次调用 f(),返回 1
std::cout << f() << '\n'; // 第三次调用 f(),返回 2
}
// 输出:
// 0
// 1
// 2
含义
yield表示挂起并返回一个值,下次继续执行时会从挂起处恢复。i是生成器的内部状态,会在多次调用之间保留。- 因为
i从 0 开始,每次yield i++:- 第一次 → 返回 0
- 第二次 → 返回 1
- 第三次 → 返回 2
以此类推。
在 C++ 里怎么实现?
- 如果是 C++20 协程,可以用
co_yield来实现类似效果。 - 如果是手动实现,可以用静态变量或迭代器对象保存状态。
例如用static模拟:
int f() {
static int i = 0; // 静态变量,跨调用保存
return i++;
}
输出同样是:
0
1
2
如果你需要的话,我可以帮你把这个 yield 版本用 C++20 协程语法 co_yield 改写,这样就完全能在现代 C++ 里跑起来了。
这个代码逐层剖开,这样你就能完全理解它是怎么模拟 Python/C# 里的 yield 效果的。
#include <coroutine>
#include <iostream>
#include <optional>
// 模板生成器类,支持生成类型为 T 的值
template <typename T>
struct Generator {
// 协程 promise 类型:控制协程的状态和数据
struct promise_type {
T current_value; // 存储当前生成的值
// 当协程中执行 co_yield 时调用
// 保存当前值,并挂起协程
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
// 协程开始时立即挂起,等待外部第一次 resume
std::suspend_always initial_suspend() { return {}; }
// 协程结束时挂起(final_suspend 保证 handle 可被安全销毁)
std::suspend_always final_suspend() noexcept { return {}; }
// 返回协程句柄包装成的 Generator 对象
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
// 协程返回时不返回值
void return_void() {}
// 如果协程中抛出异常,直接终止程序
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle; // 协程句柄
// 构造函数:保存协程句柄
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
// 析构函数:销毁协程,释放资源
~Generator() {
if (handle) handle.destroy();
}
// 获取下一个值
std::optional<T> next() {
// 如果协程句柄无效或已结束,返回空
if (!handle || handle.done()) return std::nullopt;
// 恢复协程执行,直到下一个 co_yield
handle.resume();
// 如果恢复后协程结束了,返回空
if (handle.done()) return std::nullopt;
// 返回当前生成的值
return handle.promise().current_value;
}
};
// 用协程生成器实现 yield 效果
Generator<int> f() {
int i = 0;
while (true)
co_yield i++; // 每次挂起并返回当前 i,下一次恢复时 i 自增
}
int main() {
auto gen = f(); // 获取生成器对象(协程处于初始挂起状态)
std::cout << *gen.next() << '\n'; // 恢复协程,输出 0
std::cout << *gen.next() << '\n'; // 再次恢复协程,输出 1
std::cout << *gen.next() << '\n'; // 再次恢复协程,输出 2
}
sequenceDiagram
participant M as main
participant G as Generator
participant C as Coroutine (f)
participant P as promise_type
M->>C: Call f()
C->>P: Create promise_type
P->>G: get_return_object() creates Generator with handle
P-->>C: initial_suspend() returns suspend_always
C-->>M: Return Generator object (suspended)
Note over C: Coroutine suspended at initial_suspend
M->>G: Call next()
G->>C: handle.resume()
C->>P: Execute co_yield i++ (i=0)
P-->>C: yield_value(0) stores current_value=0, suspend_always
C-->>G: Coroutine suspended
G-->>M: Return std::optional<int>(0)
M->>M: std::cout << 0 << '\n'
M->>G: Call next()
G->>C: handle.resume()
C->>P: Execute co_yield i++ (i=1)
P-->>C: yield_value(1) stores current_value=1, suspend_always
C-->>G: Coroutine suspended
G-->>M: Return std::optional<int>(1)
M->>M: std::cout << 1 << '\n'
M->>G: Call next()
G->>C: handle.resume()
C->>P: Execute co_yield i++ (i=2)
P-->>C: yield_value(2) stores current_value=2, suspend_always
C-->>G: Coroutine suspended
G-->>M: Return std::optional<int>(2)
M->>M: std::cout << 2 << '\n'
Note over G: Generator destructor called
G->>C: handle.destroy()
C->>P: Destroy coroutine
1. 总体功能
这个代码用 C++20 协程 实现了一个“生成器”(Generator),能像 Python 的 yield 一样,每次调用 next() 生成一个新的值,并记住上一次的状态。
Generator<int> f() {
int i = 0;
while (true) co_yield i++;
}
- 第一次调用
next()→ 返回0 - 第二次调用
next()→ 返回1 - 第三次调用
next()→ 返回2 - 状态不会丢失,
i会继续递增
2. 协程核心结构
promise_type
struct promise_type {
T current_value;
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
...
};
promise_type是 C++ 协程的核心协议,编译器自动帮你连接协程逻辑和外部控制代码。yield_value(T)是co_yield触发时调用的函数- 把值保存到
current_value - 返回
std::suspend_always→ 让协程暂停,等待外部再次恢复
- 把值保存到
协程生命周期
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
initial_suspend()→ 控制协程启动时是否立即暂停,这里选择暂停,等你第一次resume()再开始final_suspend()→ 控制协程结束后是否暂停,这里也暂停,方便做清理
协程返回对象
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
- 当
f()被调用时,编译器会自动生成一个promise_type,并调用这个函数返回Generator对象 std::coroutine_handle是一个可以恢复/销毁协程的指针
3. Generator 本身
std::optional<T> next() {
if (!handle || handle.done()) return std::nullopt;
handle.resume(); // 恢复协程执行
if (handle.done()) return std::nullopt;
return handle.promise().current_value;
}
handle.resume()让协程从上一次co_yield暂停的地方继续执行- 协程遇到
co_yield→ 保存值 → 暂停 → 返回给调用者 - 下次调用
next()又会从暂停处继续往下执行
4. f() 生成器函数
Generator<int> f() {
int i = 0;
while (true) co_yield i++;
}
- 这是一个无限生成整数的协程
- 每次
co_yield暂停,i的值被保存到current_value next()恢复后,i继续递增,不会重新从 0 开始
5. 执行流程(以三次 next() 为例)
f()被调用 → 返回一个暂停的协程对象gen- 第一次
next()- 恢复到
int i = 0→co_yield 0 - 暂停并返回
0
- 恢复到
- 第二次
next()- 从
co_yield之后继续 →i = 1→co_yield 1 - 暂停并返回
1
- 从
- 第三次
next()- 从
co_yield之后继续 →i = 2→co_yield 2 - 暂停并返回
2
- 从
6. 输出结果
0
1
2
如果你愿意,我可以帮你画一个协程状态切换时序图,你会更直观地理解 co_yield 是如何暂停/恢复的。这样你以后写生成器会非常顺手。
这一长段内容,其实是 Timur Doumler 的一个演讲/讲义里关于 C++ 协程(coroutine)和生成器(generator)的例子和解释,我帮你分段整理成理解版:
1. 从最朴素的 yield 想法开始
int f() {
int i = 0;
while (true)
yield i++;
}
这个是“假想语法”,不是 C++ 标准的东西,意思是“每次调用 f(),返回当前的 i,然后挂起,下次再调用时接着执行 i++ 后的值”。
但 C++ 没有
yield关键字,所以这只是概念演示。
2. 用 C++20 co_yield 语法改写
int f() {
int i = 0;
while (true)
co_yield i++;
}
co_yield是 C++ 协程的一个关键字,语义和yield类似:
- 返回一个值
- 挂起协程执行
- 等下一次恢复协程时从挂起点继续执行
但是,这样写在 C++20 是编译不过的,因为 C++20 协程语法只是“机制”,没有内置生成器类型,你需要自己定义返回类型(如 my_generator<int>)才能用。
3. 使用自定义生成器类型
my_generator<int> f() {
int i = 0;
while (true)
co_yield i++;
}
int main() {
auto g = f();
std::cout << g() << '\n';
std::cout << g() << '\n';
std::cout << g() << '\n';
}
// 输出:0 1 2
这里
my_generator<int>是用户自定义的类型,内部实现了:
promise_type(协程承诺对象)
std::coroutine_handle
- 迭代接口(或调用运算符
())
这样
g()每次调用都会从协程挂起点恢复,继续执行i++。
4. 实现原理(协程框架)
协程会被编译器拆分成:
- 用户代码(你写的逻辑)
- promise_type(协程状态管理)
std::coroutine_handle(控制协程生命周期)- 协程帧(Coroutine frame)(保存局部变量、挂起点、返回值等)
协程挂起后,会把i等局部变量保存在协程帧中,下次恢复时直接接着运行。
5. 如果不用协程,也能实现类似效果
int main() {
auto g = [i = 0]() mutable {
return i++;
};
std::cout << g() << '\n';
std::cout << g() << '\n';
std::cout << g() << '\n';
}
// 输出:0 1 2
用闭包捕获变量,也能实现“状态保存”的效果,只是没有
co_yield的挂起/恢复语义,也不能异步。
6. 现实情况
- C++20 只是提供了协程机制,没有内置生成器类型。
- C++23 开始标准库引入了
std::generator。
这段代码,是 Timur Doumler 讲解如何在 C++20/experimental 协程里自己实现生成器(generator)的例子。我帮你逐步解读:
1. promise_type 的作用
template <typename T>
struct promise_type {
T current_value; // 保存协程当前生成的值
auto get_return_object() {
return generator(*this); // 返回与 promise 绑定的 generator 对象
}
auto initial_suspend() {
return std::experimental::suspend_always{}; // 协程刚开始时挂起
}
auto final_suspend() {
return std::experimental::suspend_always{}; // 协程结束时挂起(可被销毁)
}
auto yield_value(const T& value) {
current_value = value; // 保存 yield 的值
return std::experimental::suspend_always{}; // 每次 co_yield 后挂起
}
void return_void() {} // 协程没有返回值
void unhandled_exception() {
std::terminate(); // 协程内部异常处理
}
};
理解:
promise_type是协程的核心状态管理类。
current_value保存每次co_yield的值。
initial_suspend和final_suspend控制协程开始/结束时是否挂起。
yield_value在co_yield时被调用,保存值并挂起。
get_return_object返回一个和 promise 绑定的 generator 对象给用户使用。
2. generator 类型
template <typename T>
struct generator {
struct promise_type; // 前置声明
using handle_type = std::coroutine_handle<promise_type>; // 协程句柄
handle_type handle; // 持有协程状态的句柄
generator(promise_type& promise)
: handle(handle_type::from_promise(promise)) {} // 根据 promise 构造句柄
generator(const generator&) = delete; // 禁止拷贝
generator& operator=(const generator&) = delete;
generator(generator&& other) // 移动构造
: handle(other.handle) {
other.handle = nullptr;
}
generator& operator=(generator&& other) { // 移动赋值
if (this != &other) {
handle = other.handle;
other.handle = nullptr;
}
return *this;
}
~generator() { // 析构时销毁协程
if (handle) handle.destroy();
}
T operator()() { // 调用运算符,用于获取下一个值
assert(handle != nullptr);
handle.resume(); // 恢复协程执行
return handle.promise().current_value; // 返回当前 yield 的值
}
};
理解:
generator是一个用户可以直接使用的“生成器对象”。
- 内部持有一个
std::coroutine_handle<promise_type>来控制协程的挂起/恢复。
- 禁止拷贝,只允许移动。
- 调用
operator()会让协程继续执行到下一个co_yield,然后返回生成的值。
- 析构时,协程状态会被销毁,避免内存泄漏。
3. 协程运行流程
co_yield被调用 →yield_value保存值并挂起协程。generator::operator()被调用 → 恢复协程执行。- 协程继续执行下一条语句,直到再次
co_yield或结束。 - 协程结束 →
final_suspend被触发,可安全销毁。
4. 核心特点
- 保存局部变量状态(i++)无需全局变量。
- 可多次调用
generator()获取连续值。 - 和 Python、C# 的生成器语义类似。
- 在 C++20 中需要自己定义生成器类型(C++23 将引入标准库生成器)。
如果你愿意,我可以帮你写一个完整可编译的例子,直接使用这个generator类生成 0、1、2 的序列,让你可以马上运行和测试。
sequenceDiagram
participant Main as main()
participant Generator as generator<int>
participant Promise as promise_type
Main->>Generator: auto g = f() // 创建协程
Generator->>Promise: get_return_object()
Note over Generator,Promise: 协程对象生成,尚未执行
Main->>Generator: g() // 第一次调用
Generator->>Promise: resume() // 执行协程直到第一个 co_yield
Promise->>Promise: current_value = 0
Promise-->>Generator: suspend
Generator-->>Main: return current_value (0)
Main->>Generator: g() // 第二次调用
Generator->>Promise: resume() // 从上次暂停点继续
Promise->>Promise: current_value = 1
Promise-->>Generator: suspend
Generator-->>Main: return current_value (1)
Main->>Generator: g() // 第三次调用
Generator->>Promise: resume() // 从上次暂停点继续
Promise->>Promise: current_value = 2
Promise-->>Generator: suspend
Generator-->>Main: return current_value (2)
你给出的例子是一个 典型的编译器流水线(reader → lexer → parser)使用 C++ 协程生成器 (co_yield) 的模式。下面我帮你用详细理解一下每一层:
1⃣ Reader 层
generator<char> get_char() {
while (!eof) {
// 读取下一个字符
co_yield c;
}
}
- 作用:从输入流中逐个字符读取数据。
- 机制:
- 每次
co_yield c会把当前字符返回给调用者,同时暂停协程。 - 下次调用生成器时,从暂停点继续读取下一个字符。
- 每次
- 特点:
- 类似于“惰性读取”,不会一次性把整个文件读到内存。
- 对于大文件或流式输入非常适用。
2⃣ Lexer(词法分析器)
generator<token> get_token() {
while (auto c = get_char()) {
// 根据字符生成 token
co_yield t;
}
}
- 作用:把字符流转换为 token 流(词法单元)。
- 机制:
- 内部调用
get_char()从 reader 获取字符。 - 解析字符构造 token,例如标识符、数字、符号等。
- 每次
co_yield t返回一个 token 并暂停。
- 内部调用
- 特点:
- 也是惰性生成 token,每次只生成一个 token。
- 可以直接用 for/while 遍历生成器,无需一次性生成整个 token 列表。
3⃣ Parser(语法分析器)
generator<ast_node> parse_expression() {
auto t = get_token();
// 根据 token 解析表达式
co_yield node;
}
- 作用:把 token 流解析为 AST(抽象语法树)节点。
- 机制:
- 内部调用
get_token()获取 token。 - 按语法规则生成 AST 节点。
- 每次
co_yield node返回一个 AST 节点并暂停。
- 内部调用
- 特点:
- 可以边解析边处理 AST,支持流式处理。
- 不需要先把整个输入解析完再处理结果。
总结
- 这是一个 三层流水线:
- Reader:字符流
- Lexer:token 流
- Parser:AST 流
- 优势:
- 使用协程和
co_yield可以做到懒计算(lazy evaluation)。 - 每一层都可以暂停和恢复,处理大输入时效率高。
- 逻辑清晰,每层只关注自己任务,耦合度低。
- 使用协程和
#include <coroutine>
#include <iostream>
#include <string>
#include <cassert>
// -------------------- 通用 generator --------------------
template <typename T>
struct generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
handle_type handle;
struct promise_type {
T current_value;
auto get_return_object() { return generator{*this}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
auto yield_value(const T& value) {
current_value = value;
return std::suspend_always{};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
generator(promise_type& p) : handle(handle_type::from_promise(p)) {}
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
generator(generator&& other) : handle(other.handle) { other.handle = nullptr; }
generator& operator=(generator&& other) {
if (this != &other) {
handle = other.handle;
other.handle = nullptr;
}
return *this;
}
~generator() {
if (handle) handle.destroy();
}
T operator()() {
assert(handle != nullptr);
handle.resume();
return handle.promise().current_value;
}
};
// -------------------- Reader: 生成字符 --------------------
generator<char> get_char(const std::string& input) {
for (char c : input) co_yield c;
}
// -------------------- Lexer: 生成 token --------------------
enum class TokenType { Number, Plus, Minus, Unknown };
struct Token {
TokenType type;
char value;
};
generator<Token> get_token(generator<char>& chars) {
while (true) {
char c = chars();
if (c == '\0') break; // 假设 '\0' 表示结束
if (isdigit(c))
co_yield Token{TokenType::Number, c};
else if (c == '+')
co_yield Token{TokenType::Plus, c};
else if (c == '-')
co_yield Token{TokenType::Minus, c};
else
co_yield Token{TokenType::Unknown, c};
}
}
// -------------------- Parser: 生成 AST 节点 --------------------
struct ASTNode {
std::string type;
char value;
};
generator<ASTNode> parse_expression(generator<Token>& tokens) {
while (true) {
Token t = tokens();
if (t.type == TokenType::Number)
co_yield ASTNode{"Number", t.value};
else if (t.type == TokenType::Plus)
co_yield ASTNode{"Plus", t.value};
else if (t.type == TokenType::Minus)
co_yield ASTNode{"Minus", t.value};
else
break; // 遇到 Unknown 或结束
}
}
// -------------------- 主函数 --------------------
int main() {
std::string input = "1+2-3";
auto char_gen = get_char(input);
auto token_gen = get_token(char_gen);
auto ast_gen = parse_expression(token_gen);
std::cout << "AST nodes:\n";
for (int i = 0; i < 5; ++i) { // 遍历前 5 个节点
ASTNode node = ast_gen();
std::cout << node.type << " -> " << node.value << "\n";
}
return 0;
}
你贴的三段图展示了 C++ 协程(普通 generator 与异步 generator) 的执行流程,以及它们如何解决传统回调嵌套的问题。下面我用逐段解释:
1⃣ 普通 generator (my_generator<T>)
call ───────────────────────▶ my_generator<T> f() {
//
yield ◀────────────────────┐ // code
└──────────┌───────────────┐
│ co_yield x; │
┌─────────▶└───────────────┘
resume ────────────────────┘
// more code
┌────────────── co_return x;
│
return ◀──────────────┘ }
理解:
- call:调用生成器
f(),协程开始执行。 - yield:执行到
co_yield x时暂停,将x返回给调用者。 - resume:下一次调用生成器时,从暂停点继续执行。
- co_return:协程结束,返回最终结果。
这就是一个同步 generator 的基本控制流:可以生成一系列值,每次调用返回一个。
2⃣ 异步 generator (async_generator<T> + co_await)
call ───────────────────────▶ async_generator<T> f1() { async_generator<U> f2() {
// code await // code
suspend ◀──────────┐ ┌─────────────────────────────┐────────────────────▶┌─────────────────┐
└────────── │ auto u = co_await f2(); │ │ co_yield u; │
└─────────────────────────────┘ ◀───────────────────└─────────────────┘
// more code resume }
┌─────────── co_return u;
return ◀───────────┘
}
理解:
- f1 调用 f2 并使用
co_await:- 协程
f1在co_await处 挂起,等待f2生成值。
- 协程
- f2 执行
co_yield:- 返回值
u给f1。
- 返回值
- resume:
f1继续执行co_await后的代码。
- co_return:
f1协程结束,返回最终值。
与同步 generator 不同,异步 generator 可以在等待另一个协程时挂起,不阻塞线程。
3⃣ 异步协程 + 多线程 (解决 callback hell)
Thread 1
call ───────────────────────▶ async_generator<T> f1() {
register
// code callback
suspend ◀──────────┐ ────────────────────▶ async_generator<U> f2() {
└────────── auto u = co_await f2();
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// more code ◀─────────────────── // code
call
┌────────── return u; callback co_yield u;
return ◀──────────┘
} }
Thread 2
场景概述
- 有两个异步协程:
f1(返回async_generator<T>)在 Thread 1。f2(返回async_generator<U>)在 Thread 2。
f1中使用co_await f2(),意味着f1会等待f2生成值。
执行流程
1⃣ Thread 1: 调用 f1
call ──────────────▶ async_generator<T> f1()
f1开始执行。- 执行一些代码(
// code)。 - 遇到
auto u = co_await f2();时 挂起,因为它等待f2的结果。
suspend ◀──────────┐
└── f1 挂起
2⃣ Thread 2: 执行 f2
────────────────────▶ async_generator<U> f2()
f2在 Thread 2 注册了一个 callback(事件触发时调用)。- 执行
// code后通过co_yield u生成值。 - 这个值会被
f1的co_await捕获,触发f1的恢复。
3⃣ Thread 1: 恢复 f1
resume
f1接收到f2的结果u。f1继续执行co_await后的代码(// more code)。- 最终通过
return u;或co_return结束协程。
return ◀──────────┘
总结
- Thread 1 调用
f1,遇到co_await f2()挂起。 - Thread 2 执行
f2,生成值u并通过co_yield返回。 - Thread 1 恢复执行,得到
f2的结果,继续f1的逻辑。
这一系列函数都是在判断一个整数是否是 2 的幂次方。我来详细解释:
函数原理
函数形式:
#include <iostream>
template <typename T>
bool is_power_of_2(T i) {
//
return i > 0 && (i & (i - 1)) == 0;
}
int main() {
int num = 4;
std::cout << is_power_of_2(4) << '\n';
}
步骤解析
i > 0- 2 的幂次方都是正整数,所以首先判断
i是否大于 0。 - 如果
i <= 0,直接返回false。
- 2 的幂次方都是正整数,所以首先判断
i & (i - 1)- 这是判断是否是 2 的幂的经典位运算技巧。
- 原理:
- 2 的幂的二进制表示中,只有一位是 1,其余都是 0。
例如:- 1 →
0001 - 2 →
0010 - 4 →
0100 - 8 →
1000
- 1 →
- 当你做
i - 1时:- 1 → 0 →
0000 - 2 → 1 →
0001 - 4 → 3 →
0011 - 8 → 7 →
0111
- 1 → 0 →
- 再与原数做按位与
i & (i - 1):- 2 的幂次方的结果总是
0。 - 非 2 的幂次方会至少有一位为 1,不为 0。
- 2 的幂次方的结果总是
- 2 的幂的二进制表示中,只有一位是 1,其余都是 0。
- 组合起来
i > 0 && (i & (i - 1)) == 0- 只有正整数且满足
i & (i-1) == 0时,才是 2 的幂。
为什么有很多重载
你列出的函数有:
short, unsigned short, int, unsigned int,
long, unsigned long, long long, unsigned long long,
char, signed char, unsigned char, wchar_t
这是为了 支持各种整数类型,保证不同类型的整数都可以调用 is_power_of_2。
总结理解
- 功能:判断一个整数是否是 2 的幂次方。
- 方法:用位运算
(i & (i-1)) == 0检测二进制中是否只有一位为 1。 - 注意事项:0 或负数都不是 2 的幂。
- 适用类型:支持所有常见整数类型,包括有符号和无符号类型。
两段代码是对 模板函数 is_power_of_2 的类型安全改进的例子,我来详细解析:
第一段代码
template <typename T>
bool is_power_of_2(T i) {
return i > 0 && (i & (i - 1)) == 0;
}
int main() {
std::cout << is_power_of_2(0.25);
// error: invalid operands to binary expression
}
理解
- 你写了一个模板函数,理论上可以接受任意类型
T。 - 但是
0.25是double类型(浮点数),而(i & (i-1))是 位运算。 - 浮点数不支持位运算,所以编译器报错:
error: invalid operands to binary expression - 也就是说,这个模板函数不适合浮点类型。
第二段代码
template <typename T>
bool is_power_of_2(T i) {
static_assert(std::is_integral_v<T>);
return i > 0 && (i & (i - 1)) == 0;
}
int main() {
std::cout << is_power_of_2(0.25);
// error: static_assert failed due to requirement 'std::is_integral_v<double>'
}
理解
- 使用
static_assert(std::is_integral_v<T>):- 编译期检查类型
T是否是整数类型(int, long, unsigned int等)。 - 如果不是整数类型,编译器会报错。
- 编译期检查类型
- 当你传入
0.25(double)时:std::is_integral_v<double>为falsestatic_assert触发编译错误:static_assert failed due to requirement 'std::is_integral_v<double>'
- 优点:
- 提前在编译期阻止浮点类型使用此函数。
- 不会出现不合法的位运算错误,更安全。
总结
- 原理:
is_power_of_2只对整数类型有效。 - 第一段:没有类型限制,浮点数会在编译时报位运算错误。
- 第二段:使用
static_assert限制类型为整数,浮点数会在编译期报错,更安全。
代码分析
#include <iostream>
#include <cmath>
template <typename T>
bool is_power_of_2(T i) {
static_assert(std::is_integral_v<T>);
return i > 0 && (i & (i - 1)) == 0;
}
template <typename T>
bool is_power_of_2(T x) {
static_assert(std::is_floating_point_v<T>);
int exponent;
const T mantissa = std::frexp(x, &exponent);
return mantissa == T(0.5);
}
int main() { std::cout << is_power_of_2(0.25); }
理解
- 你定义了两个模板函数,名字都是
is_power_of_2:- 第一个:针对整数类型,使用 位运算 判断是否是 2 的幂。
- 第二个:针对浮点类型,使用
std::frexp判断是否是 2 的幂。
- 问题:
- C++ 模板函数 不能仅靠
static_assert来区分重载。 - 编译器在解析模板时,看到两个
is_power_of_2(T)都是同样的签名(同样的模板参数T),所以报错:error: redefinition of function template ‘is_power_of_2’
- C++ 模板函数 不能仅靠
- 根本原因:
static_assert只在 实例化模板时 才会触发,但模板签名在编译器看来是相同的。- 因此编译器认为你 重新定义了同名模板函数。
正确做法
在 C++ 中,要根据类型做模板区分,通常用 SFINAE 或 std::enable_if / concepts:
例如 C++20 的写法:
#include <concepts>
#include <cmath>
#include <iostream>
// 整数版本
template <std::integral T>
bool is_power_of_2(T i) {
return i > 0 && (i & (i - 1)) == 0;
}
// 浮点数版本
template <std::floating_point T>
bool is_power_of_2(T x) {
int exponent;
T mantissa = std::frexp(x, &exponent);
return mantissa == T(0.5);
}
int main() {
std::cout << is_power_of_2(8) << '\n'; // 整数,输出 1
std::cout << is_power_of_2(0.25) << '\n'; // 浮点数,输出 1
}
std::integral和std::floating_point是 concepts。- 编译器可以正确选择整数版本或浮点版本,避免模板重定义错误。
这段代码实现了 判断浮点数是否是 2 的幂,我们逐行理解如下:
template <std::floating_point T>
bool is_power_of_2(T x) {
- 这是一个模板函数,要求
T必须是浮点类型(如float、double、long double)。 - 这使用了 C++20 的 concepts,
std::floating_point确保模板只能用在浮点类型上。
int exponent;
T mantissa = std::frexp(x, &exponent);
std::frexp是一个标准库函数,用于 将浮点数分解成尾数和指数:- 对于
x != 0,它返回一个mantissa和exponent,使得:x = mantissa * 2^exponent - 并且
mantissa的范围是[0.5, 1.0)。 - 例如:
double m; int e; m = std::frexp(0.25, &e); // m = 0.5, e = -1
- 对于
return mantissa == T(0.5);
- 判断尾数是否等于 0.5。
- 为什么 0.5?
- 因为如果一个浮点数是 2 的幂(如 1/8 = 0.125、8、16 等),
frexp会把它拆成0.5 * 2^n的形式。 - 例子:
8.0 → mantissa=0.5, exponent=40.25 → mantissa=0.5, exponent=-1
- 所以只要
mantissa == 0.5,就说明原数是 2 的幂。
- 因为如果一个浮点数是 2 的幂(如 1/8 = 0.125、8、16 等),
总结理解
- 这个函数只针对浮点数。
- 用
frexp将数拆成尾数和指数。 - 如果尾数是 0.5,则说明这个浮点数是 2 的幂。
- 返回
true或false。
好的,我给你的代码加上详细的注释,逐行说明每一部分的作用:
#include <cmath> // 包含数学库,使用 std::frexp
#include <iostream> // 包含输入输出库,用于 std::cout
// -------------------- 整数版本 --------------------
// 对整数类型 T 判断是否为 2 的幂
template <typename T>
auto is_power_of_2(T i) -> std::enable_if_t<std::is_integral_v<T>, bool> {
// std::enable_if_t<std::is_integral_v<T>, bool>:
// 只有当 T 是整数类型时,这个函数模板才有效,否则编译失败
// 判断方法:i > 0 && (i & (i - 1)) == 0
// 原理:
// - 2 的幂在二进制中只有一位是 1,减 1 后再与自身 & 运算结果为 0
return i > 0 && (i & (i - 1)) == 0;
}
// -------------------- 浮点数版本 --------------------
// 对浮点类型 T 判断是否为 2 的幂
template <typename T>
auto is_power_of_2(T x) -> std::enable_if_t<std::is_floating_point_v<T>, bool> {
// std::enable_if_t<std::is_floating_point_v<T>, bool>:
// 只有当 T 是浮点类型时,这个函数模板才有效
int exponent; // 用于接收 frexp 输出的指数
const T mantissa = std::frexp(x, &exponent); // 将 x 拆解为 mantissa * 2^exponent
// frexp 返回的 mantissa 范围为 [0.5, 1.0)
// 如果 x 是 2 的幂,mantissa 一定等于 0.5
return mantissa == T(0.5);
}
int main() {
std::cout << is_power_of_2(0.25); // 调用浮点数版本
// 0.25 = 2^-2, frexp(0.25) 返回 mantissa=0.5, exponent=-1
// 所以输出 1 (true)
}
核心理解:
- 整数版本:用二进制特性
(i & (i-1)) == 0判断 2 的幂。 - 浮点版本:用
frexp拆分尾数和指数,尾数为 0.5 就是 2 的幂。 enable_if:根据类型选择不同的模板实现,防止错误类型调用。
1⃣ is_power_of_2 多种写法演变
你展示了很多版本的 is_power_of_2 模板,从 C++11/14 的 enable_if 到 C++20 的 concepts:
(1) enable_if 方式
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
bool is_power_of_2(T i) { ... }
- 这里
enable_if用于 SFINAE(Substitution Failure Is Not An Error),限制模板只在 整数类型可用。 - 同理还有浮点版本:
template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
bool is_power_of_2(T x) { ... }
- 问题:写法复杂,可读性差,而且容易出“重定义”错误,如果两个模板参数默认值相同。
(2) C++20 Concepts 方式
template <std::integral T>
bool is_power_of_2(T i) { ... }
template <std::floating_point T>
bool is_power_of_2(T x) { ... }
std::integral和std::floating_point是 C++20 的 概念(concepts)。- 更加简洁直观,编译器会在不满足条件时报错。
(3) 使用 auto + Concepts
bool is_power_of_2(std::integral auto i) { ... }
template <std::floating_point T>
bool is_power_of_2(T x) { ... }
- 结合
auto和 concepts,使调用更加简洁。 - 编译器会根据参数类型选择正确的模板版本。
2⃣ 自定义 Concepts 示例
template <typename T>
concept arithmetic = std::integral<T> || std::floating_point<T>;
template <typename T>
concept my_number = arithmetic<T> && sizeof(T) <= 8;
arithmetic:约束 T 为整数或浮点数。my_number:进一步约束 T 大小不超过 8 字节(排除 long double)。- 结合
auto:
auto f(my_number auto x) { ... }
- 调用示例:
auto x = f(2.0); // OK,double 符合 my_number
auto y = f(2.0L); // error,long double 不符合 sizeof(T) <= 8
3⃣ hashable Concept
template <typename T>
concept hashable = requires(T t) {
{ std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};
template <hashable T>
class hash_map;
- 定义一个概念
hashable:要求类型 T 可以用std::hash生成std::size_t。 - 限制模板类
hash_map只能接受可哈希的类型作为键。
总结
- SFINAE + enable_if:C++11/14 的老方法,可行但繁琐。
- Concepts:C++20 新特性,更安全、可读性强。
- 自定义 Concepts:可定义复杂约束,如类型大小、是否可哈希。
- 模板函数选择:通过 concepts,编译器在编译期根据类型自动选择正确版本。
1⃣ 按年龄排序 std::sort / std::ranges::sort
struct User {
std::string name;
int age;
};
std::vector<User> users = { /* ... */ };
int main() {
auto sort_by_age = [] (auto& lhs, auto& rhs) {
return lhs.age < rhs.age;
};
std::sort(users.begin(), users.end(), sort_by_age);
}
std::sort对users按age排序。- 使用 lambda 表达式作为比较函数。
auto& lhs, auto& rhs是 C++14/17 的通用 lambda 写法。
C++20 ranges 改写:
std::ranges::sort(users, std::less{}, &User::age);
- 使用 投影(projection),直接用
&User::age表示按成员排序。 - 更简洁,不用写 lambda。
2⃣ std::copy_if + std::transform 过滤和映射
bool underage(const User& user) { return user.age < 18; }
int main() {
std::vector<User> filtered_users;
std::copy_if(users.begin(), users.end(), std::back_inserter(filtered_users),
std::not_fn(underage)); // 保留 18 岁及以上
std::transform(filtered_users.begin(), filtered_users.end(),
std::ostream_iterator<int>(std::cout, "\n"),
[] (const auto& user) { return user.age; });
}
std::copy_if过滤出 成年用户(年龄 ≥ 18)。std::transform映射出age并打印。std::not_fn(underage)取underage函数的逻辑反(成年用户)。
3⃣ C++20 ranges + views 改写
auto result = users
| std::views::filter(std::not_fn(underage))
| std::views::transform([] (const auto& user) { return user.age; });
std::ranges::copy(result, std::ostream_iterator<int>(std::cout, "\n"));
std::views::filter:延迟过滤,生成一个视图(view)。std::views::transform:映射操作。std::ranges::copy输出到std::ostream_iterator。- 相比上一个例子,更现代化、延迟计算,不生成额外中间容器。
4⃣ range-based for 循环遍历 views
for (auto& age : users
| std::views::filter(std::not_fn(underage))
| std::views::transform([] (const auto& user) { return user.age; })) {
std::cout << age << '\n';
}
- 直接在
for循环里遍历视图。 - 避免显式调用
std::ranges::copy,语义清晰。
5⃣ iota 生成序列
for (int i : std::views::iota(0, 10))
std::cout << i << '\n';
std::views::iota(0, 10)生成一个 0 到 9 的整数序列。- 类似 Python 的
range(0, 10)。 - C++20 的 ranges 提供了很多延迟生成序列的工具,非常方便。
输出:
0
1
2
3
4
5
6
7
8
9
总结
- 排序:
- 传统
std::sort+ lambda。 - C++20
std::ranges::sort+ 投影(projection)。
- 传统
- 过滤 & 映射:
- 传统:
std::copy_if+std::transform。 - C++20:
std::views::filter+std::views::transform+std::ranges::copy或 range-based for。
- 传统:
- 生成序列:
std::views::iota可以生成整数范围序列,延迟计算。
- 理念:
- C++20 ranges 追求 延迟计算 + 组合式 pipeline,避免中间容器。
- views 提高代码可读性,表达逻辑更直观。
更多推荐


所有评论(0)