45、C++23 前瞻:std::generator,协程生成器,简化迭代器编写。

说实话,每次写自定义迭代器,我都觉得像是在做体力活。你得定义五个类型别名,重写 operator++operator*operator!=…… 一套组合拳下来,代码量比业务逻辑还多。我早期做流式数据处理框架时,光迭代器适配层就写了上千行,后来重构时自己都看不下去了。

C++23 带来的 std::generator,说白了就是让你用协程的方式“吐”数据,编译器帮你把迭代器的脏活累活全干了。你只需要写一个生成器函数,剩下的交给标准库。

从“手动挡”到“自动挡”

我们先看一个最直观的例子。假设你要生成一个斐波那契数列的迭代器。传统写法长这样:

class FibIterator {
    int a_{0}, b_{1};
    int idx_{0}, limit_;
public:
    using iterator_category = std::input_iterator_tag;
    using value_type = int;
    using difference_type = std::ptrdiff_t;
    using pointer = const int*;
    using reference = const int&;

    FibIterator(int limit) : limit_(limit) {}
    int operator*() const { return a_; }
    FibIterator& operator++() {
        int next = a_ + b_;
        a_ = b_; b_ = next;
        ++idx_;
        return *this;
    }
    bool operator!=(const FibIterator& other) const {
        return idx_ != other.idx_;
    }
    // ... 还有一堆 boilerplate
};

嗯,这还只是最简版本。你要是想支持 const 迭代、反向迭代、随机访问…… 那代码量直接翻倍。我在项目中见过有人为了一个树形结构的遍历,写了四个迭代器类,最后自己都分不清哪个是哪个。

std::generator 怎么写?看这里:

#include <generator>
#include <ranges>

std::generator<int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

// 使用
for (int v : fibonacci(10)) {
    std::cout << v << " ";
}
// 输出: 0 1 1 2 3 5 8 13 21 34

看到了吗?没有迭代器类,没有运算符重载,没有类型别名。一个 co_yield 搞定所有。编译器会自动生成一个符合 input_range 规范的迭代器。你想想看,这省了多少事。

std::generator 的核心机制

为什么能这么简洁?因为 std::generator 本质上是一个协程包装器。它把协程的挂起/恢复机制,映射成了迭代器的 ++* 操作。

核心流程:

  1. 调用生成器函数时,协程启动,执行到第一个 co_yield 处挂起。
  2. operator* 返回当前挂起点的值。
  3. operator++ 恢复协程,执行到下一个 co_yield 或结束。
  4. 协程结束时,迭代器变为 end 状态。

说白了,co_yield 就是“暂停一下,把值给你,等你取走了我再继续”。这个模型特别适合那些“按需生成”的场景——比如读取大文件的行、遍历树形结构、生成无穷序列。

下面这张图展示了 std::generator 的完整生命周期:

std::generator 生命周期与数据流 调用者 (for 循环) 请求下一个值 std::generator 协程帧 协程函数体 co_yield / co_return operator++ 恢复协程 co_yield 返回值 协程挂起 关键状态转换 ① 开始:协程启动,执行到第一个 co_yield ② 迭代:每次 operator++ 恢复协程,operator* 读取当前值 ③ 结束:协程执行完毕或 co_return,迭代器变为 end

实战:树形结构的懒遍历

我在做 AST(抽象语法树)分析工具时,经常需要遍历树节点。传统做法是写一个递归函数,把所有节点塞进一个 vector 里返回。但这样有两个问题:一是内存占用大,二是无法提前终止遍历。

std::generator 可以做到“遍历到哪,生成到哪”。来看一个二叉树的中序遍历:

struct Node {
    int value;
    Node* left;
    Node* right;
};

std::generator<int> inorder(Node* root) {
    if (!root) co_return;
    
    // 遍历左子树
    for (int v : inorder(root->left)) {
        co_yield v;
    }
    
    // 访问当前节点
    co_yield root->value;
    
    // 遍历右子树
    for (int v : inorder(root->right)) {
        co_yield v;
    }
}

// 使用
Node* tree = buildTree();
for (int v : inorder(tree)) {
    std::cout << v << " ";
    if (v > 100) break;  // 可以提前终止
}

注意看,这里递归调用 inorder 返回的生成器,然后通过 co_yield 把值“透传”出去。这在传统迭代器里几乎不可能优雅实现——你得手动维护一个栈来模拟递归。而协程天然支持嵌套挂起,递归生成器写起来就跟普通递归一样自然。

我的经验: 递归生成器虽然方便,但要注意协程帧的嵌套深度。每个 co_yield 都会创建一个协程帧,深度递归可能导致栈溢出。我一般限制递归深度不超过 1000 层,或者改用显式栈的迭代方式。

与现有标准库的配合

std::generator 实现了 std::ranges::input_range,所以它能直接用在 <ranges> 的管道操作中:

auto even_fibs = fibonacci(20) 
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::take(5);

for (int v : even_fibs) {
    std::cout << v << " ";  // 输出前5个偶数斐波那契数
}

这组合起来就非常强大了。生成器负责“生产”,视图负责“加工”,消费者负责“使用”。整个数据流是懒执行的,不会产生中间容器。

特性 传统迭代器 std::generator
代码量 50-100 行(含 boilerplate) 5-10 行
递归遍历 需要手动维护栈 天然支持递归
状态管理 成员变量 + 构造函数 局部变量 + 协程帧
提前终止 需要额外标志位 直接 break 即可
与 ranges 配合 需手动适配 开箱即用

避坑指南

好东西归好东西,用起来还是有几个坑要注意。我曾经在一个生产环境里踩过,分享给你:

我曾经犯过的错:

  • 不要跨作用域传递引用: co_yield 返回的是引用还是值,取决于生成器模板参数。如果你写 std::generator<int&>,返回的是局部变量的引用——协程挂起后局部变量可能被销毁,变成悬空引用。我建议默认都用值传递,除非你非常清楚自己在做什么。
  • 注意协程的销毁成本: 每个 std::generator 对象销毁时,会释放协程帧。如果你创建了大量短生命周期的生成器(比如在循环里反复构造),性能可能会受影响。我的做法是:如果生成器生命周期很短,考虑用普通函数+回调代替。
  • 不要假设生成器是可复制或可重入的: std::generator 是 move-only 类型,而且每个生成器只能被一个消费者遍历一次。你不能像普通容器那样拷贝一份再遍历。这在多线程场景下尤其要注意。

总结

std::generator 是 C++23 里我最期待的特性之一。它把协程的威力带到了日常的迭代器编写中,让“生产-消费”模式的代码变得异常简洁。我个人习惯在以下场景优先考虑它:

  • 需要懒生成序列(斐波那契、素数、排列组合等)
  • 需要遍历递归数据结构(树、图、AST)
  • 需要从 I/O 流中按需读取数据(大文件行读取、网络包解析)
  • 需要与 <ranges> 配合做数据管道处理

当然,它也不是万能的。对于性能极其敏感的场景(比如每秒调用百万次迭代),手写迭代器可能还是更优选择。但对于 90% 的日常需求,std::generator 已经足够好用了。

嗯,C++ 这些年一直在变复杂,但 std::generator 这种特性,是真正让复杂变简单的。希望你在下一个项目里也能用上它。


专注资料整理