C++co_yield生成器协程实现

2026-04-11 20:50:30 951阅读 0评论

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),把阻塞等待变成非阻塞拉取,把“预分配+遍历”变成“边算边交”。下次遇到需要逐项处理又不想爆内存的场景,不妨试试把它从工具箱里拿出来——不是为了炫技,而是因为它真的更省、更稳、更贴近问题本质。

写完这个生成器,我顺手重写了那个分页模块。上线后监控曲线平滑得像尺子画的,运维发来消息:“这次没告警,谢谢。”——有时候,技术的价值就藏在这种安静的落地里。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,951人围观)

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

目录[+]