C++co_yield生成器协程实现
C++20协程实战:用 co_yield 写一个真正能跑的生成器
上周帮同事调一个内存暴涨的旧模块,发现他用 vector 预存了上万组分页查询结果——其实只需要逐个处理。我顺手改成了 generator<int>,内存从 300MB 掉到不到 2MB,CPU 时间还少了 18%。这让我想起很多开发者对 co_yield 的误解:它不是语法糖,而是一套可控的、栈无关的、按需生产的执行流控制机制。
C++20 协程不是“让函数暂停”,而是把函数拆成状态机 + 恢复点。co_yield 正是这个机制里最接地气的一环——它不返回值,而是把当前计算出的一个元素“推”给调用方,然后主动挂起自己,等下次被拉取时再从下一行继续执行。
要写出能用的生成器,三样东西缺一不可:
- 自定义 promise_type(决定协程怎么启动、挂起、结束)
- generator 类模板(封装迭代器接口,让用户像用 range 一样用)
- 正确的
co_yield使用姿势(不是所有地方 yield 都安全)
先看 promise_type 的关键逻辑。它必须提供 initial_suspend()、final_suspend()、unhandled_exception(),以及最重要的 yield_value(T&& v)。注意:yield_value 返回的是 suspend_always{} 或 suspend_never{},决定了每次 yield 后是否真的挂起。如果你返回 suspend_never,协程会一路冲到底,co_yield 就退化成普通赋值——这点很多人踩过坑。
template<typename T>
struct generator_promise {
T current_value;
std::exception_ptr except;
auto get_return_object() { return generator<T>{*this}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() { except = std::current_exception(); }
auto yield_value(T&& v) {
current_value = std::move(v);
return std::suspend_always{}; // 关键:必须挂起,才能实现“按需生产”
}
};
generator 类本身不存储数据,只持有一个指向 promise 的指针,并提供 begin()/end() 和 operator++。它的 iterator 是一个轻量 wrapper,每次 ++ 就 resume 协程一次,直到遇到 final_suspend。
真正的价值体现在使用侧。比如写一个斐波那契生成器:
generator<long long> fib(int n) {
long long a = 0, b = 1;
for (int i = 0; i < n; ++i) {
if (i == 0) co_yield a;
else if (i == 1) co_yield b;
else {
auto next = a + b;
co_yield next;
a = b;
b = next;
}
}
}
调用时就这么简单:
for (auto x : fib(10)) {
std::cout << x << " "; // 输出 0 1 1 2 3 5 8 13 21 34
}
但这里有个隐藏细节:co_yield 不是函数调用,不能出现在 lambda 捕获列表、static_assert 或 sizeof 中。曾有位同学想在 yield 前加日志,写了 co_yield log_and_return(x);,结果编译失败——因为 log_and_return 是普通函数,而 co_yield 要求表达式必须可移动且不能含未定义行为。正确做法是先算好值,再 yield:
auto val = expensive_calc(x);
log("produced: ", val);
co_yield val; // 安全
另一个实用技巧:用 co_yield 实现带状态的过滤器。比如读取大文件,每行 yield 一个有效整数:
generator<int> parse_ints(std::istream& is) {
std::string line;
while (std::getline(is, line)) {
try {
int x = std::stoi(line);
co_yield x; // 只有成功解析才产出
} catch (...) {
continue; // 跳过非法行,不中断整个流程
}
}
}
这种写法比把所有行读进 vector 再 filter 干净得多——内存恒定,错误隔离,且支持无限流(比如网络响应体)。
最后提醒一个易忽略的事实:co_yield 产生的对象生命周期由 promise 管理,不是由调用栈管理。所以别 co_yield local_array[0] 这种操作——局部变量在协程挂起后就销毁了。要用 co_yield std::move(some_string) 或直接 yield 值类型。
协程不是银弹,但 co_yield 是少数几个能立刻带来可观收益的 C++20 特性。它不改变算法复杂度,却能把 O(n) 空间压成 O(1),把阻塞等待变成非阻塞拉取,把“预分配+遍历”变成“边算边交”。下次遇到需要逐项处理又不想爆内存的场景,不妨试试把它从工具箱里拿出来——不是为了炫技,而是因为它真的更省、更稳、更贴近问题本质。
写完这个生成器,我顺手重写了那个分页模块。上线后监控曲线平滑得像尺子画的,运维发来消息:“这次没告警,谢谢。”——有时候,技术的价值就藏在这种安静的落地里。


还没有评论,来说两句吧...