C++20新特性:协程

之前我们已经学习过进程和线程。本期我们就来接触一个计算机方面的新概念:协程。

相关代码提交:楼田莉子/Linux学习

目录

协程介绍

协程与普通函数

协程与线程

核心差异

协程与线程的关系模型

C++20协程概述

关键字

设计哲学

[无栈协程 vs 有栈协程](#无栈协程 vs 有栈协程)

协程核心组件及其作用

[Promise 对象(promise_type)](#Promise 对象(promise_type))

协程句柄(std::coroutine_handle

[Awaitable 与 Awaiter](#Awaitable 与 Awaiter)

[协程帧(Coroutine Frame)](#协程帧(Coroutine Frame))

std::coroutine_traits

协程函数的完整执行流程

阶段一:协程创建与启动

[阶段二:协程体执行与 co_await 流程](#阶段二:协程体执行与 co_await 流程)

[阶段三:co_yield 流程](#阶段三:co_yield 流程)

阶段四:协程结束与销毁

[对称转移(Symmetric Transfer)详解](#对称转移(Symmetric Transfer)详解)

关键设计决策与常见陷阱

代码示例


协程介绍

协程是一种能够暂停执行并在稍后恢复 的泛型函数组件。它不同于普通函数的一次性调用-返回模型,而是提供了一种可多次挂起(Suspend)和恢复(Resume) 的执行流,允许你以近乎同步代码的方式编写异步逻辑,从而在保持高可读性的同时,获得异步执行的性能优势。

协程被明确为一种语言级别的特性 ,通过三个关键字 co_awaitco_yieldco_return 标识。编译器会为协程生成一个状态机,将局部变量、挂起点信息打包进一个堆分配(可优化)的帧中。当协程执行到 co_await 表达式时,它可以选择挂起,将控制权返还给调用者或调度器;当等待的事件就绪,协程可以从挂起点之后继续执行。整个过程不涉及操作系统内核的调度,完全在用户态完成。

协程的三大核心特性

  • 协作式(Cooperative) :协程只有在显式调用挂起点(如 co_await)时才会让出执行权,不能被外部强制中断。这与线程的抢占式调度形成鲜明对比。

  • 用户态管理:协程的调度、切换完全由应用程序或库代码控制,不经过系统调用,没有陷入内核的开销。

  • 有栈(Stackful)与无栈(Stackless)之分:C++20实现的是无栈协程,状态保存在堆分配的帧中,不依赖独立的运行栈,轻量且性能极高。其他语言如Go的goroutine则偏向有栈实现。

协程与普通函数

本质区别 :普通函数是子例程(subroutine) ------调用者将控制权完全交给被调函数,被调函数执行完成后控制权返回。协程是协作式多任务的基本单元------协程可以在执行中途主动让出控制权(suspend),由调用者或调度器在合适的时机恢复执行(resume)。这种"协作式"语义使得协程特别适合编写异步代码、生成器、惰性求值等场景。

维度 普通函数 协程
调用模型 一次性调用,执行完毕后返回,栈帧销毁 可多次挂起(suspend)和恢复(resume),状态在挂起点之间保持
栈帧 分配在调用栈上,出作用域即销毁 分配在堆上(通常),生命周期跨越多次挂起/恢复
控制流 单进单出:进入 → 执行 → 返回 多进多出:进入 → 挂起 → 返回调用方 → 恢复 → 继续执行 → ...
局部变量 仅在单次调用期间存活 跨挂起点的局部变量保存在协程帧中,在多次恢复之间保持值
关键字 return co_returnco_yieldco_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{};
    }
};

关键设计决策与常见陷阱

  1. final_suspend 务必返回 suspend_always 如果返回 suspend_never,协程在 final_suspend 后自动销毁,外部持有的 coroutine_handle 将变为悬空指针,任何操作都是未定义行为。

  2. await_suspend 中保存 coroutine_handle 这是将协程注册到事件循环/调度器的关键时机。通常在此将 handle 存入 IOCP/epoll 的完成队列或其他异步框架中。

  3. co_await 不可出现在 catch 块中(C++20): C++20 标准不允许在 catch 块中使用 co_await,这是协程与异常处理交互的一个已知限制(C++23 可能放宽)。

  4. 协程的生命周期管理: 调用者通过 coroutine_handle::done()destroy() 管理协程生命周期。如果 final_suspend 返回 suspend_always,必须有人调用 destroy(),否则内存泄漏。

  5. 参数生命周期: 协程参数(值传递)被拷贝到协程帧中。但如果通过引用传递,调用者必须确保引用在协程的整个生命周期内有效------这是常见的悬挂引用陷阱。

代码示例

一个序列生成器,当调用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;
}

封面图自取:

相关推荐
xiaoshuaishuai81 小时前
C# AvaloniaUI 中旋转
开发语言·c#
Dream_ksw1 小时前
Python 基础
开发语言·python
元宝骑士1 小时前
SpringBoot + Sa-Token 实现 CSRF 令牌校验(进阶篇)
后端·安全
Full Stack Developme1 小时前
AspectJ 详解
java·后端
weixin_428005301 小时前
C#调用 AI学习从0开始-第2阶段(Function Calling+工具调用智能体)-第9天实战-实现计算器工具
开发语言·学习·c#·functioncalling·ai实现计算器工具
Deepoch1 小时前
Deepoc VLA开发板:除草机器人的持续学习与协同作业系统
人工智能·学习·机器人·开发板·具身模型·deepoc
炘爚1 小时前
phase1:基础框架——编译 + MySQL + 登录/注册
linux·c++
武子康1 小时前
Java-20 深入浅出 MyBatis - 手写ORM框架1 从原始 JDBC 暴露的 6 大问题开始
java·后端
雪隐2 小时前
AI股票小助手06-Backtrader 量化回测
人工智能·后端