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

怎么在 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_value或await_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 自动管理。








