之前我们已经学习过进程和线程。本期我们就来接触一个计算机方面的新概念:协程。
相关代码提交:楼田莉子/Linux学习
目录
[无栈协程 vs 有栈协程](#无栈协程 vs 有栈协程)
[Promise 对象(promise_type)](#Promise 对象(promise_type))
[Awaitable 与 Awaiter](#Awaitable 与 Awaiter)
[协程帧(Coroutine Frame)](#协程帧(Coroutine Frame))
[阶段二:协程体执行与 co_await 流程](#阶段二:协程体执行与 co_await 流程)
[阶段三:co_yield 流程](#阶段三:co_yield 流程)
[对称转移(Symmetric Transfer)详解](#对称转移(Symmetric Transfer)详解)
协程介绍
协程是一种能够暂停执行并在稍后恢复 的泛型函数组件。它不同于普通函数的一次性调用-返回模型,而是提供了一种可多次挂起(Suspend)和恢复(Resume) 的执行流,允许你以近乎同步代码的方式编写异步逻辑,从而在保持高可读性的同时,获得异步执行的性能优势。
协程被明确为一种语言级别的特性 ,通过三个关键字 co_await、co_yield、co_return 标识。编译器会为协程生成一个状态机,将局部变量、挂起点信息打包进一个堆分配(可优化)的帧中。当协程执行到 co_await 表达式时,它可以选择挂起,将控制权返还给调用者或调度器;当等待的事件就绪,协程可以从挂起点之后继续执行。整个过程不涉及操作系统内核的调度,完全在用户态完成。
协程的三大核心特性:
-
协作式(Cooperative) :协程只有在显式调用挂起点(如
co_await)时才会让出执行权,不能被外部强制中断。这与线程的抢占式调度形成鲜明对比。 -
用户态管理:协程的调度、切换完全由应用程序或库代码控制,不经过系统调用,没有陷入内核的开销。
-
有栈(Stackful)与无栈(Stackless)之分:C++20实现的是无栈协程,状态保存在堆分配的帧中,不依赖独立的运行栈,轻量且性能极高。其他语言如Go的goroutine则偏向有栈实现。
协程与普通函数
本质区别 :普通函数是子例程(subroutine) ------调用者将控制权完全交给被调函数,被调函数执行完成后控制权返回。协程是协作式多任务的基本单元------协程可以在执行中途主动让出控制权(suspend),由调用者或调度器在合适的时机恢复执行(resume)。这种"协作式"语义使得协程特别适合编写异步代码、生成器、惰性求值等场景。
| 维度 | 普通函数 | 协程 |
|---|---|---|
| 调用模型 | 一次性调用,执行完毕后返回,栈帧销毁 | 可多次挂起(suspend)和恢复(resume),状态在挂起点之间保持 |
| 栈帧 | 分配在调用栈上,出作用域即销毁 | 分配在堆上(通常),生命周期跨越多次挂起/恢复 |
| 控制流 | 单进单出:进入 → 执行 → 返回 | 多进多出:进入 → 挂起 → 返回调用方 → 恢复 → 继续执行 → ... |
| 局部变量 | 仅在单次调用期间存活 | 跨挂起点的局部变量保存在协程帧中,在多次恢复之间保持值 |
| 关键字 | return |
co_return、co_yield、co_await |
| 返回类型 | 任意类型 | 必须满足协程 trait 要求(内部需定义 promise_type) |
| 调用者视角 | 调用即阻塞,等待结果 | 调用立即返回一个"未来结果"的句柄对象,调用者异步获取结果 |
协程与线程
核心差异
| 维度 | 线程 | 协程 |
|---|---|---|
| 调度方式 | 操作系统抢占式调度 | 协作式调度(用户代码主动让出) |
| 切换成本 | 内核态上下文切换(~1-10μs) | 纯用户态切换(~几 ns 到几十 ns) |
| 栈空间 | 每个线程独立栈(通常 1-8 MB) | 协程帧在堆上,按需分配(通常远小于线程栈) |
| 并发性 | 真正并行(多核) | 并发非并行(单线程内交替执行) |
| 同步原语 | 互斥锁、条件变量、信号量等 | 无需锁(单线程内无竞态) |
| 创建数量 | 受系统限制(通常数百到数千) | 可创建数百万个 |
| 由谁管理 | 内核管理生命周期 | 用户代码管理(通过 coroutine_handle) |
协程与线程的关系模型
┌─────────────────────────────────────────────────────────┐
│ 进程 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 线程 1 │ │ 线程 2 │ │ 线程 N │ ... │
│ │ │ │ │ │ │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │协程A │ │ │ │协程C │ │ │ │协程E │ │ │
│ │ │协程B │ │ │ │协程D │ │ │ │协程F │ │ │
│ │ └──────┘ │ │ └──────┘ │ │ │协程G │ │ │
│ │ │ │ │ │ └──────┘ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
- M:1 模型(单线程多协程): 一个线程上运行多个协程,协程之间通过挂起/恢复交替执行。这是最简单的模型,不需要任何同步机制。
- M:N 模型(多线程多协程): 多个协程分布在多个线程上,需要一个调度器(如
asio::io_context)来管理协程在线程间的分配。此模型可以充分利用多核 CPU。 - 协程可以在线程之间迁移: 一个协程可以在线程 A 上挂起,然后在线程 B 上恢复。这要求协程内部的数据访问是线程安全的。
总结:协程是"用户态的轻量级协作式执行单元",线程是"内核态的抢占式执行单元"。协程解决了"大量并发任务"的组织和表达问题,线程解决了"真正并行计算"的硬件利用问题。两者互补,而非替代
C++20协程概述
C++20 引入的协程是无栈协程(stackless coroutine),具有以下核心特征:
关键字
C++20引入的协程主要有三大关键字
| 关键字 | 语义 |
|---|---|
co_await |
挂起当前协程,等待某个操作完成;恢复后获取结果 |
co_yield |
挂起协程并向外返回一个值(用于生成器模式) |
co_return |
结束协程执行,可选择返回一个最终值 |
判定规则: 函数体内出现以上三个关键字中的任何一个,编译器即将其识别为协程函数,并触发协程变换。
设计哲学
C++20 协程不提供完整的协程库,而是提供了最低限度的语言级基础设施 ------即编译器将协程函数体变换为一个状态机所需的协议和接口。用户需要自己实现(或使用第三方库提供的)promise_type、awaiter 等组件。这种设计给予库作者极大的灵活性:
- 生成器(Generator): 惰性产生值序列(
co_yield) - 异步任务(Task): 表示一个异步操作(
co_await) - 惰性求值(Lazy): 延迟计算,仅在需要时执行
无栈协程 vs 有栈协程
| 特性 | C++20 无栈协程 | 有栈协程(如 Boost.Coroutine2) |
|---|---|---|
| 状态存储 | 编译器在堆上分配协程帧 | 独立的调用栈 |
| 嵌套调用 | 只能在顶层挂起(不能从内部函数中挂起) | 可在任意深度的嵌套调用中挂起 |
| 内存开销 | 编译器精确计算所需大小,按需分配 | 预分配固定大小的栈 |
| 性能 | 切换极快,局部性好 | 需要切换整个栈 |
协程核心组件及其作用
C++20 协程框架定义了以下关键组件,它们共同构成了协程的执行基础设施:
Promise 对象(promise_type)
Promise 是协程内部控制中心 ,由编译器在协程帧内创建。它管理协程的状态转换和值传递。必须在协程的返回类型中定义为嵌套类型 promise_type。
其必需/可选接口如下:
cpp
struct promise_type {
// ────── 1. 获取返回对象 ──────
// 必须。编译器调用它获取协程的返回对象,交给调用者
auto get_return_object() -> ReturnType;
// ────── 2. 初始挂起点控制 ──────
// 必须。决定协程在开始执行前是否挂起
// 返回 suspend_never → 立即执行("热启动")
// 返回 suspend_always → 挂起等待外部 resume()("惰性启动")
auto initial_suspend() -> std::suspend_always; // 或其他 awaitable
// ────── 3. 最终挂起点控制 ──────
// 必须。决定协程结束后的行为
// 通常返回 suspend_always → 保持协程帧存活,由外部 destroy()
auto final_suspend() noexcept -> std::suspend_always; // 或其他 awaitable
// ────── 4. 返回值处理(二选一) ──────
void return_value(T value); // co_return expr 时调用
void return_void(); // co_return; 或自然结束时调用
// ────── 5. yield 值处理 ──────
// co_yield expr 时调用,expr 作为参数传入
// 返回值是一个 awaitable,决定挂起行为
auto yield_value(T value) -> std::suspend_always; // 或其他 awaitable
// ────── 6. 异常处理 ──────
// 协程体抛出未捕获异常时调用
void unhandled_exception();
// ────── 7. await_transform(可选) ──────
// 每当协程体内出现 co_await expr,编译器会调用此函数转换 expr
// 允许 promise 定制所有 await 行为
template<typename U>
auto await_transform(U&& expr) -> SomeAwaitable;
};
协程句柄(std::coroutine_handle<P>)
coroutine_handle 是外部操控协程的唯一入口。 它是协程帧的轻量级指针(类似 void* 大小)。
cpp
template<typename Promise = void>
struct coroutine_handle {
// ── 工厂方法 ──
static coroutine_handle from_promise(Promise& p); // 从 promise 获取句柄
// ── 生命周期操作 ──
void resume(); // 恢复协程执行(从上次挂起点继续)
void destroy(); // 销毁协程帧(释放堆内存)
bool done() const; // 协程是否已完成(在 final_suspend 之后为 true)
// ── Promise 访问 ──
Promise& promise() const; // 获取 promise 引用
// ── 地址操作 ──
void* address() const; // 取得协程帧地址
static coroutine_handle from_address(void*); // 从地址恢复句柄
// ── 空状态 ──
explicit operator bool() const; // 检查句柄是否有效
};
关键点:
resume()必须仅在协程处于挂起状态时调用,否则是未定义行为。通常在resume()之前应检查done()或通过 awaiter 的设计来保证。
Awaitable 与 Awaiter
这是 co_await 背后的协议,定义了挂起和恢复的完整语义。
概念层级:
bash
Awaitable(可等待对象)
└── 实现 operator co_await() → 返回 Awaiter
或
└── 自身就是 Awaiter(实现了 await_ready/await_suspend/await_resume)
Awaiter(等待器)
└── 实现三个方法控制挂起/恢复语义
Awaiter 三部曲:
cpp
struct Awaiter {
// 步骤1:是否可以直接继续(跳过挂起)?
// 返回 true → 跳过 suspend,直接执行 await_resume
// 返回 false → 进入 suspend 流程
bool await_ready() const;
// 步骤2:挂起时做什么?(仅在 await_ready 返回 false 时调用)
// 返回值类型有四种可能:
// void → 挂起协程,控制权返回调用方/上一级恢复者
// bool → true 挂起;false 不挂起(与 await_ready 互补)
// coroutine_handle<P> → 对称转移:不经过调用方,直接恢复目标协程
void await_suspend(std::coroutine_handle<P> h);
// 步骤3:恢复时获取结果
// 协程被 resume() 后在 await_resume 处继续执行
// 返回值作为 co_await 表达式的值
T await_resume();
};
标准预定义 Awaiter:
cpp
struct suspend_never {
bool await_ready() const noexcept { return true; } // 直接继续,不挂起
void await_suspend(std::coroutine_handle<>) const noexcept {} // 不会被调用
void await_resume() const noexcept {}
};
struct suspend_always {
bool await_ready() const noexcept { return false; } // 总是挂起
void await_suspend(std::coroutine_handle<>) const noexcept {} // 挂起,控制权返回
void await_resume() const noexcept {}
};
协程帧(Coroutine Frame)
协程帧是编译器在堆上分配的一块连续内存,包含以下内容:
bash
┌─────────────────────────────┐
│ Promise 对象 │ ← coroutine_handle::promise()
├─────────────────────────────┤
│ 被拷贝的函数参数 │ ← 值传递的参数拷贝
├─────────────────────────────┤
│ 当前挂起点标识 │ ← 状态机的"当前状态编号"
├─────────────────────────────┤
│ 跨挂起点的局部变量 │ ← 所有在 co_await/co_yield 之后仍使用的变量
├─────────────────────────────┤
│ 临时变量(如有) │
└─────────────────────────────┘
内存分配: 编译器使用
operator new分配协程帧。如果promise_type提供了类内operator new,则优先使用之。如果编译器可以证明协程帧的生命周期不会超出调用方,它被允许优化掉堆分配(HALO, Heap Allocation Elision)。
std::coroutine_traits
用于将返回类型映射到 promise_type:
cpp
template<typename R, typename... Args>
struct coroutine_traits {
using promise_type = typename R::promise_type;
// 默认行为:从返回类型 R 中取 promise_type
};
可以通过特化来为非侵入式类型添加协程支持:
cpp
// 例如让 std::future<T> 支持协程(假设性示例)
template<typename T, typename... Args>
struct coroutine_traits<std::future<T>, Args...> {
struct promise_type { /* ... */ };
};
协程函数的完整执行流程
以下是编译器将一个包含 co_await/co_yield/co_return 的函数变换后的实际执行序列。理解这个流程是掌握 C++20 协程的关键。
阶段一:协程创建与启动
调用者调用 MyCoroutine(arg1, arg2, ...)
│
▼
┌─────────────────────────────────────────────────────┐
│ 1. 通过 coroutine_traits 确定 promise_type │
│ using Promise = std::coroutine_traits< │
│ ReturnType, Arg1, Arg2>::promise_type; │
├─────────────────────────────────────────────────────┤
│ 2. 分配协程帧 │
│ - 查找 promise_type::operator new(优先) │
│ - 否则使用全局 operator new │
│ - 分配大小 = 编译期确定的协程帧大小 │
├─────────────────────────────────────────────────────┤
│ 3. 将函数参数拷贝/移动到协程帧中 │
│ - 值参数:拷贝到帧中(此后函数体内的参数引用 │
│ 实际指向帧内的拷贝) │
│ - 引用参数:拷贝引用本身到帧中 │
├─────────────────────────────────────────────────────┤
│ 4. 在协程帧内构造 Promise 对象 │
│ - 调用 Promise 的构造函数 │
│ - 参数可以传递给 Promise 构造函数 │
│ (如果 Promise 构造函数接受匹配的参数) │
├─────────────────────────────────────────────────────┤
│ 5. 调用 promise.get_return_object() │
│ - 获得协程的返回值对象 │
│ - 此返回值将在 initial_suspend 之后返回给调用者 │
│ - 通常在此建立 coroutine_handle 与返回值对象的关联 │
├─────────────────────────────────────────────────────┤
│ 6. 执行 co_await promise.initial_suspend() │
│ ┌─ 如果返回 suspend_never: │
│ │ → 立即继续到步骤 7(热启动) │
│ │ │
│ └─ 如果返回 suspend_always: │
│ → 挂起,控制权返回到步骤 7 │
│ (协程体尚未执行任何用户代码) │
├─────────────────────────────────────────────────────┤
│ 7. 将 get_return_object() 的结果返回给调用者 │
│ - 调用者此时获得了协程的返回对象 │
│ - 协程可能已经部分执行,也可能尚未开始 │
└─────────────────────────────────────────────────────┘
阶段二:协程体执行与 co_await 流程
协程体开始执行(或由外部 resume() 恢复)
│
▼
执行用户代码,直到遇到 co_await expr
│
▼
┌─────────────────────────────────────────────────────────┐
│ 步骤A:构建 Awaitable │
│ │
│ 1. 如果 promise_type 有 await_transform(expr): │
│ Awaitable = promise.await_transform(expr) │
│ 2. 否则: Awaitable = expr │
│ │
├─────────────────────────────────────────────────────────┤
│ 步骤B:获取 Awaiter 对象 │
│ │
│ 1. 如果 Awaitable 有 operator co_await(): │
│ Awaiter = Awaitable.operator co_await() │
│ 2. 否则: Awaiter = Awaitable │
│ (即 Awaitable 自身就是 Awaiter) │
│ │
├─────────────────────────────────────────────────────────┤
│ 步骤C:检查是否需要挂起 │
│ │
│ 调用 bool ready = awaiter.await_ready() │
│ │
│ ┌─ ready == true: │
│ │ → 跳过挂起,直接跳转到步骤F(await_resume) │
│ │ → 这允许在结果已就绪时避免挂起开销 │
│ │ │
│ └─ ready == false: │
│ → 进入步骤D(真正挂起) │
├─────────────────────────────────────────────────────────┤
│ 步骤D:挂起协程 │
│ │
│ 调用 awaiter.await_suspend(coroutine_handle) │
│ 将当前协程的 handle 传给 awaiter │
│ │
│ 根据 await_suspend 的返回值决定行为: │
│ │
│ ┌─ 返回 void: │
│ │ → 控制权返回给调用方(即上一次 resume 的来源) │
│ │ │
│ ├─ 返回 bool true: │
│ │ → 挂起,控制权返回给调用方 │
│ │ │
│ ├─ 返回 bool false: │
│ │ → 不挂起,立即恢复当前协程(极少用) │
│ │ │
│ └─ 返回 coroutine_handle<Q>: │
│ → 对称转移(symmetric transfer) │
│ → 不经过上一级调用方,直接恢复目标协程 │
│ → 这是高效协程调度的关键:避免调用栈膨胀 │
├─────────────────────────────────────────────────────────┤
│ (外部世界此时可以执行其他代码) │
│ (...) │
│ │
│ 当外部调用 coroutine_handle::resume(): │
│ → 协程从步骤E继续 │
├─────────────────────────────────────────────────────────┤
│ 步骤E:协程被恢复 │
│ │
│ resume() 内部执行流回到协程体,继续执行用户代码 │
│ │
├─────────────────────────────────────────────────────────┤
│ 步骤F:获取 await 结果 │
│ │
│ auto result = awaiter.await_resume() │
│ │
│ 整个 co_await expr 表达式的求值结果就是 result │
│ 协程在此处继续执行后续的用户代码 │
└─────────────────────────────────────────────────────────┘
阶段三:co_yield 流程
co_yield expr 是语法糖,编译器将其等价变换为:
co_yield expr
|
| 编译器变换 ↓
v
co_await promise.yield_value(expr)
执行过程:
┌──────────────────────────────────────────────────────┐
│ 1. 调用 promise.yield_value(expr) │
│ - 通常将 expr 存储到 promise 的某个成员中 │
│ - 返回值是一个 awaitable(如 suspend_always) │
│ │
│ 2. 对返回值进行 co_await │
│ - 标准流程(如 suspend_always → 挂起) │
│ │
│ 3. 协程挂起,调用者通过 promise 或返回值对象获取 expr │
│ │
│ 4. 调用者 resume() 后,协程从 co_yield 下一行继续 │
└──────────────────────────────────────────────────────┘
阶段四:协程结束与销毁
协程体执行到 co_return expr / co_return / 自然结束 / 异常逃逸
│
▼
┌─────────────────────────────────────────────────────────┐
│ 情况1:co_return expr │
│ → promise.return_value(expr) │
│ │
│ 情况2:co_return; 或 自然流到函数末尾 │
│ → promise.return_void() │
│ │
│ 情况3:未捕获异常逃出协程体 │
│ → promise.unhandled_exception() │
│ → 内部通常通过 std::current_exception() 获取异常 │
├─────────────────────────────────────────────────────────┤
│ 无论哪种情况,接下来都执行: │
│ │
│ 1. 按构造的逆序销毁所有局部变量和临时对象 │
│ - 包括跨挂起点的变量(它们在协程帧内) │
│ │
│ 2. 执行 co_await promise.final_suspend() │
│ ┌─ 如果返回 suspend_always: │
│ │ → 协程保持在挂起状态,帧存活 │
│ │ → 外部调用 coroutine_handle::done() 返回 true │
│ │ → 外部负责调用 coroutine_handle::destroy() │
│ │ 以释放协程帧内存 │
│ │ → final_suspend 中常做清理/通知操作 │
│ │ │
│ └─ 如果返回 suspend_never: │
│ → 协程自动销毁(帧被释放) │
│ → 此后任何对 handle 的操作是 UB │
│ → 危险!通常不这样做 │
│ │
│ 3. 当 coroutine_handle::destroy() 被调用: │
│ → 析构 promise 对象 │
│ → 释放协程帧内存(通过 operator delete) │
│ → 协程生命周期结束 │
└─────────────────────────────────────────────────────────┘
对称转移(Symmetric Transfer)详解
对称转移是 C++20 协程中最重要的性能优化机制之一。
问题: 如果不使用对称转移,每个 co_await 挂起后通过调用方的 resume() 恢复,会导致嵌套的 resume() 调用,可能引发栈溢出:
caller.resume(hA) → hA 挂起 → caller.resume(hB) → hB 挂起 → caller.resume(hA) → ...
↑
调用栈不断增长!
解决方案: await_suspend 返回目标协程的 coroutine_handle,编译器在挂起当前协程的同时直接恢复目标协程------不经过中间调用方,实现了 O(1) 栈增长:
hA 挂起,await_suspend 返回 hB 的 handle
→ 编译器直接执行 hB.resume()
→ 调用栈不增长!
以以下伪代码为例
cpp
// 示例:一个简单的调度器
struct TaskPromise {
// ...
auto final_suspend() noexcept {
struct FinalAwaiter {
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<TaskPromise> h) noexcept {
// 将当前协程的后续任务恢复,实现对称转移
if (h.promise().continuation) {
h.promise().continuation.resume(); // 不在 await_suspend 中返回 handle
// 而是手动 resume continuation ------ 非对称转移
}
}
// 对称转移版本应返回 continuation.handle
std::coroutine_handle<> await_suspend(
std::coroutine_handle<TaskPromise> h) noexcept {
return h.promise().continuation; // 对称转移!
}
void await_resume() noexcept {}
};
return FinalAwaiter{};
}
};
关键设计决策与常见陷阱
-
final_suspend务必返回suspend_always: 如果返回suspend_never,协程在final_suspend后自动销毁,外部持有的coroutine_handle将变为悬空指针,任何操作都是未定义行为。 -
在
await_suspend中保存coroutine_handle: 这是将协程注册到事件循环/调度器的关键时机。通常在此将 handle 存入 IOCP/epoll 的完成队列或其他异步框架中。 -
co_await不可出现在catch块中(C++20): C++20 标准不允许在 catch 块中使用co_await,这是协程与异常处理交互的一个已知限制(C++23 可能放宽)。 -
协程的生命周期管理: 调用者通过
coroutine_handle::done()和destroy()管理协程生命周期。如果final_suspend返回suspend_always,必须有人调用destroy(),否则内存泄漏。 -
参数生命周期: 协程参数(值传递)被拷贝到协程帧中。但如果通过引用传递,调用者必须确保引用在协程的整个生命周期内有效------这是常见的悬挂引用陷阱。
代码示例
一个序列生成器,当调用Range(1,5,2)时会生成1,3,5这样的数字序列
cpp
#pragma once
/**
* @file Range.hpp
* @brief 基于C++20协程的泛型序列生成器 ------ Range(start, end, step)
*
* 设计思路:
* - Generator<T> 是协程返回类型,通过 co_yield 惰性产出序列元素
* - Range(start, end, step) 生成从 start 开始、到 end 结束、步长为 step 的等差序列
* - 支持整数与浮点类型,兼容 STL 迭代器接口与范围 for 遍历
* - end 为开区间(不包含),与 Python range 语义一致
*
* 使用示例:
* for (auto v : Range(1, 10, 2)) { ... } // 1, 3, 5, 7, 9
* for (auto v : Range(10, 1, -2)) { ... } // 10, 8, 6, 4, 2
* auto gen = Range(0.0, 1.0, 0.1);
* std::vector<double> vec(gen.begin(), gen.end()); // 与STL算法配合
*/
#include <coroutine>
#include <exception>
#include <utility>
#include <stdexcept>
#include <iterator>
namespace coroutine_gen {
/**
* @brief 协程生成器 ------ 惰性产生 T 类型值序列
*
* 实现要点:
* - promise_type::yield_value 在每次 co_yield 时保存值并挂起
* - initial_suspend 返回 suspend_never 实现热启动
* - final_suspend 返回自定义 Awaiter 实现对称转移
* - 提供 begin()/end() 以支持范围 for 和 STL 算法
*
* @tparam T 序列元素类型
*/
template <typename T>
class Generator {
public:
// ── promise_type:协程内部控制中心 ──
struct promise_type {
T current_value; // 最近一次 co_yield 产出的值
// 创建协程返回对象
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
// 惰性启动:调用者需显式调用 next() 或 begin() 推进协程
std::suspend_always initial_suspend() noexcept { return {}; }
// 最终挂起:协程结束后保持帧存活,对称转移回调用者
auto final_suspend() noexcept {
struct FinalAwaiter {
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(
std::coroutine_handle<promise_type> h) noexcept {
auto& p = h.promise();
// 若存在调用者且未完成,则转移控制权
if (p.caller && !p.caller.done()) {
return p.caller;
}
return std::noop_coroutine();
}
void await_resume() noexcept {}
};
return FinalAwaiter{};
}
// co_yield expr → 保存值并挂起
std::suspend_always yield_value(T value) noexcept {
current_value = value;
return {};
}
// co_return 无值
void return_void() noexcept {}
// 协程内未捕获异常
void unhandled_exception() {
exception = std::current_exception();
}
std::exception_ptr exception;
std::coroutine_handle<> caller{std::noop_coroutine()};
};
using handle_type = std::coroutine_handle<promise_type>;
// ── 构造与析构 ──
Generator() noexcept : handle_(nullptr) {}
explicit Generator(handle_type h) noexcept : handle_(h) {}
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
Generator& operator=(Generator&& other) noexcept {
if (this != &other) {
if (handle_) handle_.destroy();
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
~Generator() {
if (handle_) handle_.destroy();
}
// ── 迭代接口 ──
/// 推进协程到下一个 co_yield 点,协程结束则返回 false
bool next() {
if (!handle_ || handle_.done()) return false;
handle_.resume();
if (handle_.promise().exception) {
std::rethrow_exception(handle_.promise().exception);
}
return !handle_.done();
}
/// 获取当前 co_yield 的值(需在 next() 返回 true 后调用)
T value() const {
return handle_.promise().current_value;
}
/// 协程是否已结束
bool done() const {
return !handle_ || handle_.done();
}
// ── 范围 for 与 STL 迭代器支持 ──
/// 前向迭代器:满足 std::input_iterator 语义
/// 同时充当哨兵(handle 为 null 时表示结束),保证 begin()/end() 同类型
struct iterator {
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag; // 支持 STL 算法
using iterator_concept = std::input_iterator_tag; // C++20 iterator_concept
using pointer = T*; // std::iterator_traits 要求此成员
using reference = T&; // std::iterator_traits 要求此成员
handle_type handle;
// 与其他迭代器比较(用于 STL 算法的同类型迭代器对)
// 判断当前迭代器是否已到达序列末尾
bool is_end() const {
return !handle || handle.done();
}
bool operator==(const iterator& other) const {
// 两个都是 end → 相等
if (is_end() && other.is_end()) return true;
// 只有一个 end → 不等
if (is_end() || other.is_end()) return false;
// 都未结束且是同一个协程 → 按句柄比较
return handle == other.handle;
}
bool operator!=(const iterator& other) const {
return !(*this == other);
}
// 前置++:推进协程到下一个 co_yield 点
iterator& operator++() {
if (handle && !handle.done()) {
handle.resume();
if (handle.promise().exception) {
std::rethrow_exception(handle.promise().exception);
}
}
return *this;
}
// 后置++:推进并返回旧状态
void operator++(int) {
++(*this);
}
// 解引用:获取当前 co_yield 值
T operator*() const {
return handle.promise().current_value;
}
};
/// 返回指向第一个元素的迭代器(会触发协程执行到第一个 co_yield)
iterator begin() {
if (handle_ && !handle_.done()) {
// 协程初始处于 suspend_always 状态,需要 resume 一次到达首个 co_yield
handle_.resume();
if (handle_.promise().exception) {
std::rethrow_exception(handle_.promise().exception);
}
if (!handle_.done()) {
return {handle_};
}
}
return {nullptr};
}
/// 返回结束哨兵迭代器(handle 为 null)
iterator end() { return {nullptr}; }
private:
handle_type handle_;
};
} // namespace coroutine_gen
/**
* @brief 协程函数:生成 [start, end) 区间内步长为 step 的等差序列
*
* 语义说明:
* - step > 0 时生成值 < end
* - step < 0 时生成值 > end
* - step == 0 时若 start != end 则抛出异常(无限序列不可生成)
* - end 为开区间,与 Python range 的 stop 语义一致
*
* @tparam T 数值类型(支持整数与浮点)
* @param start 起始值(包含)
* @param end 结束值(不包含)
* @param step 步长(正数递增,负数递减)
* @return Generator<T> 协程生成器
*
* 使用示例:
* // 整数序列
* for (auto v : Range(0, 10, 2)) std::cout << v << " "; // 0 2 4 6 8
* // 递减序列
* for (auto v : Range(10, 0, -2)) std::cout << v << " "; // 10 8 6 4 2
* // 浮点序列
* for (auto v : Range(0.0, 1.0, 0.2)) std::cout << v << " "; // 0 0.2 ... 0.8
* // 与 STL 算法配合
* auto gen = Range(1, 100, 2);
* std::vector<int> vec(gen.begin(), gen.end());
*/
template <typename T>
coroutine_gen::Generator<T> Range(T start, T end, T step) {
// step == 0 且 start != end → 无法产生有穷序列,抛异常
if (step == T{0}) {
if (start != end) {
throw std::invalid_argument(
"Range: step 不能为 0(会产生无限序列)");
}
// start == end 且 step == 0 → 空序列,直接返回
co_return;
}
if constexpr (std::is_integral_v<T>) {
// 整数类型:直接比较避免浮点累积误差
if (step > T{0}) {
for (T cur = start; cur < end; cur += step) {
co_yield cur;
}
} else {
for (T cur = start; cur > end; cur += step) {
co_yield cur;
}
}
} else {
// 浮点类型:使用迭代计数避免 step 累积误差
// 计算理论上应生成的元素个数,按索引 co_yield
T range_size = (end - start) / step;
// range_size <= 0 → 空序列
if (range_size <= T{0}) {
co_return;
}
// 计算元素数量(向上取整,因为 end 为开区间)
// 例:Range(0.0, 1.0, 0.3) → range_size = 1.0/0.3 ≈ 3.33 → 4个元素?
// 不对,应该是0.0, 0.3, 0.6, 0.9 共4个,0.9+0.3=1.2>1.0
// 实际上 range_size = 3.33, ceil = 4
// 但我们需要确认是否超出
// 更稳妥:直接用计数 + 乘法计算每个值
size_t count = 0;
if (step > T{0}) {
// 正步长:start + n*step < end
// 最大 n 满足 start + n*step < end
// n < (end - start) / step
// 浮点下用 while 循环更安全但效率低
// 折中:预估数量后逐个产出并检查边界
T cur = start;
while (cur < end) {
co_yield cur;
cur = start + static_cast<T>(++count) * step;
}
} else {
T cur = start;
while (cur > end) {
co_yield cur;
cur = start + static_cast<T>(++count) * step;
}
}
}
}
测试
cpp
/**
* @file RangeTest.cpp
* @brief Range.hpp 协程序列生成器的单元测试
*
* 测试覆盖:
* 1. 正步长整数序列 --- 验证顺序、数量、边界
* 2. 负步长整数序列 --- 递减序列
* 3. 浮点序列 --- 验证浮点等差序列
* 4. 空序列(多种边界条件)
* 5. step == 0 异常处理
* 6. 范围 for 循环遍历
* 7. 与 STL 算法集成(vector构造、accumulate等)
* 8. 移动语义
* 9. 迭代器语义(前置/后置++)
* 10. 单元素序列
*/
#include "Range.hpp"
#include <iostream>
#include <vector>
#include <cassert>
#include <numeric>
#include <algorithm>
#include <cmath>
// 简易测试框架
static int g_passed = 0;
static int g_failed = 0;
#define TEST(name) \
void test_##name(); \
struct Register_##name { \
Register_##name() { run_test(#name, test_##name); } \
} reg_##name; \
void test_##name()
void run_test(const char* name, void (*fn)()) {
std::cout << " RUNNING: " << name << " ... ";
try {
fn();
++g_passed;
std::cout << "PASSED\n";
} catch (const std::exception& e) {
++g_failed;
std::cout << "FAILED (" << e.what() << ")\n";
} catch (...) {
++g_failed;
std::cout << "FAILED (unknown)\n";
}
}
// ═══════════════════════════════════════════════
// 测试用例
// ═══════════════════════════════════════════════
TEST(positive_step_integer) {
// Range(0, 10, 2) → 0, 2, 4, 6, 8
std::vector<int> values;
auto gen = Range(0, 10, 2);
while (gen.next()) {
values.push_back(gen.value());
}
std::vector<int> expected = {0, 2, 4, 6, 8};
assert(values.size() == 5);
assert(values == expected);
}
TEST(positive_step_default_start) {
// Range(0, 5, 1) → 0, 1, 2, 3, 4
std::vector<int> values;
auto gen = Range(0, 5, 1);
while (gen.next()) {
values.push_back(gen.value());
}
assert(values.size() == 5);
for (size_t i = 0; i < values.size(); ++i) {
assert(values[i] == static_cast<int>(i));
}
}
TEST(negative_step_integer) {
// Range(10, 0, -2) → 10, 8, 6, 4, 2
std::vector<int> values;
auto gen = Range(10, 0, -2);
while (gen.next()) {
values.push_back(gen.value());
}
std::vector<int> expected = {10, 8, 6, 4, 2};
assert(values.size() == 5);
assert(values == expected);
}
TEST(negative_step_all) {
// Range(5, 0, -1) → 5, 4, 3, 2, 1
std::vector<int> values;
for (auto v : Range(5, 0, -1)) {
values.push_back(v);
}
std::vector<int> expected = {5, 4, 3, 2, 1};
assert(values == expected);
}
TEST(empty_range_start_equals_end) {
// Range(5, 5, 1) → 空序列
auto gen = Range(5, 5, 1);
assert(!gen.next());
assert(gen.done());
}
TEST(empty_range_step_direction_mismatch) {
// Range(0, 10, -1) → 空序列(step方向与区间方向相反)
auto gen = Range(0, 10, -1);
assert(!gen.next());
assert(gen.done());
}
TEST(empty_range_negative_step_mismatch) {
// Range(10, 0, 1) → 空序列
auto gen = Range(10, 0, 1);
assert(!gen.next());
assert(gen.done());
}
TEST(step_zero_start_equals_end) {
// Range(5, 5, 0) → start == end,step==0,空序列
auto gen = Range(5, 5, 0);
assert(!gen.next());
assert(gen.done());
}
TEST(step_zero_start_not_equals_end) {
// Range(0, 10, 0) → 应抛出异常
bool caught = false;
try {
auto gen = Range(0, 10, 0);
gen.next(); // 触发协程执行
} catch (const std::invalid_argument&) {
caught = true;
}
assert(caught);
}
TEST(float_range_basic) {
// Range(0.0, 1.0, 0.2) → 0.0, 0.2, 0.4, 0.6, 0.8
std::vector<double> values;
for (auto v : Range(0.0, 1.0, 0.2)) {
values.push_back(v);
}
assert(values.size() == 5);
for (size_t i = 0; i < values.size(); ++i) {
double expected = i * 0.2;
assert(std::abs(values[i] - expected) < 1e-9);
}
}
TEST(float_range_negative_step) {
// Range(1.0, 0.0, -0.25) → 1.0, 0.75, 0.5, 0.25
std::vector<double> values;
for (auto v : Range(1.0, 0.0, -0.25)) {
values.push_back(v);
}
assert(values.size() == 4);
double expected_vals[] = {1.0, 0.75, 0.5, 0.25};
for (size_t i = 0; i < values.size(); ++i) {
assert(std::abs(values[i] - expected_vals[i]) < 1e-9);
}
}
TEST(range_for_loop) {
// 验证范围 for 循环遍历
int count = 0;
int sum = 0;
for (auto val : Range(1, 20, 3)) {
sum += val;
++count;
}
// 1, 4, 7, 10, 13, 16, 19 → 7 个元素,和 = 70
assert(count == 7);
assert(sum == 70);
}
TEST(stl_vector_construction) {
// 与 STL vector 构造配合
auto gen = Range(0, 100, 7);
std::vector<int> vec(gen.begin(), gen.end());
// 验证:0, 7, 14, ..., 98
assert(!vec.empty());
assert(vec[0] == 0);
assert(vec.back() == 98);
for (size_t i = 0; i < vec.size(); ++i) {
assert(vec[i] == static_cast<int>(i * 7));
}
}
TEST(stl_accumulate) {
// 与 std::accumulate 配合
auto gen = Range(1, 11, 1); // 1..10
auto it = gen.begin();
auto end = gen.end();
int sum = 0;
for (; it != end; ++it) {
sum += *it;
}
assert(sum == 55); // 1+2+...+10
}
TEST(stl_find) {
// 与 std::find 配合
auto gen = Range(2, 50, 2); // 偶数序列
auto it = std::find(gen.begin(), gen.end(), 24);
assert(it != gen.end());
assert(*it == 24);
}
TEST(move_semantics) {
// 移动构造后新对象可继续迭代
auto gen1 = Range(0, 100, 10);
assert(gen1.next()); // 取第 0 个值 → 0
assert(gen1.value() == 0);
auto gen2 = std::move(gen1);
// gen1 已被移动,gen2 继续产出后续值
int count = 1;
while (gen2.next()) {
++count;
}
assert(count == 10); // 总共 0..90,10个元素
}
TEST(single_element) {
// Range(42, 43, 1) → 仅 42
auto gen = Range(42, 43, 1);
assert(gen.next());
assert(gen.value() == 42);
assert(!gen.next());
}
TEST(large_range) {
// 大规模序列验证
auto gen = Range(0, 100000, 1);
int count = 0;
long long sum = 0;
while (gen.next()) {
sum += gen.value();
++count;
}
assert(count == 100000);
// 0..99999 求和 = n*(n-1)/2
assert(sum == static_cast<long long>(100000) * 99999 / 2);
}
TEST(iterator_pre_increment) {
// 前置++ 语义
auto gen = Range(0, 5, 1);
auto it = gen.begin();
assert(*it == 0);
++it;
assert(*it == 1);
++it;
assert(*it == 2);
}
TEST(iterator_post_increment) {
// 后置++ 语义
auto gen = Range(0, 5, 1);
auto it = gen.begin();
int val = *it;
it++;
assert(val == 0);
assert(*it == 1);
}
TEST(done_semantics) {
// done() 在迭代完成后返回 true
auto gen = Range(0, 3, 1);
assert(!gen.done());
gen.next(); // 0
assert(!gen.done());
gen.next(); // 1
gen.next(); // 2
assert(!gen.done());
gen.next(); // 将done置true
assert(gen.done());
}
// ═══════════════════════════════════════════════
int main() {
std::cout << "=== Range 协程序列生成器 单元测试 ===\n\n";
// 全局注册器自动执行所有 TEST 用例
std::cout << "\n结果: " << g_passed << " 通过, "
<< g_failed << " 失败\n";
return g_failed > 0 ? 1 : 0;
}
封面图自取:
