C++怎么使用generator_C++23协程生成器教程【迭代】

1次阅读

c++23中需手动实现generator:基于std::coroutine_handle与自定义promise_type,显式调用next(),不可直接用于range-for;co_yield挂起点由yield_value返回awaitable的await_suspend决定;须谨慎处理生命周期与跨编译器abi差异。

C++怎么使用generator_C++23协程生成器教程【迭代】

怎么在 C++23 里写一个最简 generator

标准库没提供 generator 类型,得自己定义——但别急着抄 Boost 或第三方实现。C++23 协程规范里明确要求编译器支持 co_yield 和自定义协程句柄,std::generator 是提案(P2165),尚未进标准库(截至 GCC 14 / Clang 18 / MSVC 19.38)。所以你现在能用的,是基于 std::coroutine_handle + 自定义 promise_type 的轻量 generator 模板。

最简可用版本长这样:

template<typename T> class generator {   struct promise_type;   using handle_type = std::coroutine_handle<promise_type>  public:   struct promise_type {     T current_value;     auto get_return_object() { return generator{handle_type::from_promise(*this)}; }     auto initial_suspend() { return std::suspend_always{}; }     auto final_suspend() noexcept { return std::suspend_always{}; }     void return_void() {}     auto yield_value(T v) {       current_value = std::move(v);       return std::suspend_always{};     }     void unhandled_exception() { std::terminate(); }   };  private:   handle_type h_;  public:   explicit generator(handle_type h) : h_(h) {}   ~generator() { if (h_) h_.destroy(); }   generator(const generator&) = delete;   generator& operator=(const generator&) = delete;   generator(generator&& o) noexcept : h_(o.h_) { o.h_ = {}; }   generator& operator=(generator&& o) noexcept {     if (h_) h_.destroy();     h_ = o.h_;     o.h_ = {};     return *this;   }    T next() {     if (!h_ || h_.done()) throw std::runtime_error("generator exhausted");     h_.resume();     if (h_.done()) throw std::runtime_error("generator yielded after final suspend");     return h_.promise().current_value;   } };

关键点不是“怎么写全”,而是:你必须显式调用 next(),不能直接用 range-for(除非补 begin()/end() 和迭代器);yield_value 返回 std::suspend_always 是为了确保每次 co_yield 后停住;initial_suspend 也设为 std::suspend_always,否则构造时就执行到第一个 co_yield 前,可能出未定义行为。

为什么 range-for 不能直接用 generator

因为标准 range-for 要求类型有 begin()end(),且返回的迭代器要满足 input_iterator 概念。而裸 generator 不是 range,它只是个可恢复的计算过程容器。

立即学习C++免费学习笔记(深入)”;

  • 强行加 begin()/end() 需额外封装一层(比如 generator_range),还要处理移动语义和多次遍历问题
  • 每次 range-for 都会尝试构造新迭代器,但 generator 是一次性消耗品(resume() 后无法 rewind)
  • 如果 generator 内部有局部变量或捕获状态,复制对象会导致悬垂引用或重复析构
  • Clang/GCC 当前对协程的 range 支持不一致:Clang 17+ 允许 for (auto x : f()) 如果 f() 返回带合适 begin/end 的类型;GCC 14 还会报 no viable conversion

所以别硬套语法糖。老老实实用 while + try/catch 更可控:

auto g = make_fibonacci(); while (true) {   try {     std::cout << g.next() << "n";   } catch (const std::runtime_error&) {     break;   } }

co_yield 后挂起的位置到底在哪

很多人以为 co_yield expr 等价于 “计算 expr → 存进 promise → 挂起”,其实挂起发生在 yield_value 返回的 awaitable 的 await_suspend 执行之后。也就是说,真正暂停点取决于你 yield_value 返回的那个对象的 await_suspend 实现。

  • 返回 std::suspend_always{}:立刻挂起,控制权交还给调用方
  • 返回 std::suspend_never{}:不挂起,协程继续往下跑(危险!容易溢出或逻辑错乱)
  • 如果你返回自定义 awaitable,并在 await_suspend 里调度到线程池,那挂起时机就由调度器决定——这已超出 generator 基本用途
  • 调试时注意:GDB/LLDB 对协程帧支持有限,co_yield 行可能显示为 “not executable”,实际断点得打在 yield_valueawait_suspend

MSVC / GCC / Clang 在协程 ABI 上的坑

三大编译器对协程的内存布局、promise 构造时机、异常传播路径实现不一致,尤其影响 generator 的跨平台稳定性。

  • MSVC 默认把 promise 分配在协程帧内(stack-allocated),但若 promise 有非平凡析构函数,可能触发未定义行为;建议加 #pragma clang system_header 或用 /Zc:coroutines- 关闭优化干扰
  • GCC 13–14 中,若 generator 函数参数含右值引用,co_yield 可能绑定到已销毁的临时对象(生命周期分析 bug),规避方式:全部传值或 const lvalue 引用
  • Clang 17 开始支持 -fcoroutines-ts,但若混用 libstdc++(GCC 标准库)会链接失败,必须用 libc++ 编译整个项目
  • 所有编译器都不保证协程帧分配在上——所以不要假设 generator 对象比其内部 handle 活得久;析构顺序错位极易 crash

generator 不是语法糖,它是手动管理协程生命周期的薄封装。最易被忽略的是:你永远得自己确保 promise 的 lifetime 覆盖整个 resume 过程,而不是依赖 RAII 自动管理。

text=ZqhQzanResources