代表性面试主题

C++23 面试:std::generator 如何实现惰性范围?

编程题困难
Offer.cc 编辑团队发布 更新

题干

请用 std::generator 生成一个惰性序列,并说明它何时比返回 vector、回调或 views 更合适,以及如何避免悬空引用和资源泄漏。

题干与适用场景

你需要遍历一个可能很大的树或文件流,只在消费者请求下一个元素时产生值,不能一次性把全部结果放入内存。请使用 C++23 std::generator 表达惰性范围,解释 co_yield 的执行模型、异常和对象生命周期,并比较其他实现方式。

这是一道 coding 题,重点是协程句柄、输入范围语义和资源边界。假设消费者只做单线程、单向遍历,生成器引用的外部对象在遍历完成前保持有效。

面试官考察点

  • 能否区分惰性生成、容器物化和普通 views 的职责。
  • 是否理解第一次迭代、每次 ++、结束和销毁分别发生什么。
  • 是否识别生成器引用局部变量、临时对象和异步资源的生命周期风险。
  • 能否说明异常传播、取消遍历和递归 elements_of 的代价。
  • 是否以数据量、首元素延迟、峰值内存和重复遍历需求作出选择。

回答前需要澄清的问题

  1. 消费者只遍历一次还是需要多次复用?一次遍历更适合生成器,复用可能需要容器。
  2. 元素是值、引用还是视图?引用可减少拷贝,但要求源对象活得更久。
  3. 生成过程是否会阻塞 I/O、等待事件或跨线程?std::generator 本身是同步协程,不提供异步调度。
  4. 是否要支持随机访问、大小查询或并行算法?输入范围通常不提供这些能力。
  5. 遍历中途停止时,文件、锁或其他资源如何释放?必须让生成器帧和 RAII 对象有明确归属。

30 秒回答框架

我会把生成器定义为同步、单向的输入范围,用 co_yield 在每个元素处暂停;消费者递增迭代器时协程继续,直到 co_return 或异常。它适合结果很大、只需一次顺序消费且首元素应尽快产生的场景。若需要随机访问、重复遍历或跨线程异步 I/O,我会选择 vector、view 管道或专门的异步抽象,并先验证源对象和资源的生命周期。

分步骤深入解答

1. 先确定范围和所有权

std::generator<T> 是 C++23 的同步协程范围。生成函数调用时通常只建立协程状态,真正执行从迭代开始。生成器对象拥有协程帧,消费者结束或生成器销毁时帧才释放。函数不能返回指向其局部容器的引用;若返回引用,引用的对象必须由调用方或更外层 owner 保证存活。

2. 用 co_yield 保持常量工作集

cpp
#include <generator>

std::generator<int> range(int first, int last) {
  for (int value = first; value < last; ++value) {
    co_yield value;
  }
}

void consume() {
  for (int value : range(0, 1'000'000)) {
    if (value == 10) break;
  }
}

这里没有先创建一百万个元素;每次恢复只推进到下一次 co_yieldbreak 会销毁迭代器和生成器,之后不能继续使用同一个已销毁的协程帧。实际代码要根据标准库实现和编译器版本验证 C++23 支持。

3. 比较四种实现

返回 vector 最简单,支持大小、随机访问和多次遍历,但需要完整物化。回调适合把控制权交给生产者,却难以组合范围适配器。手写输入迭代器可在旧标准中使用,但要维护状态、结束和异常协议。std::views 适合已有范围上的无状态变换;生成器适合状态机、递归遍历或外部推进才产生值的逻辑。

4. 处理递归与引用

树遍历可以让子生成器通过 elements_of 组合,避免手写嵌套循环;但深度、协程帧数量和异常路径都应测量。若 co_yield std::string_view 或节点引用,源字符串和树节点必须在整个消费过程中有效。不要从生成器返回临时字符串的 view,也不要把生成器保存到超出源对象生命周期的队列。

5. 处理异常、提前停止和资源

生成函数内异常会在恢复迭代器时传播到消费者;消费者应决定记录、重试还是终止。中途 break 不等于业务层完成了资源提交,文件句柄、锁和临时缓冲应由生成器帧内的 RAII 对象管理,并在销毁时释放。它是同步抽象,不能通过 co_yield 自动等待网络或把阻塞 I/O 变成异步。

6. 用边界基准收口

用空范围、单元素、极大范围、递归深度、异常中止和引用失效样本测试。比较 vector、generator 和 view 管道的首元素延迟、全量耗时、峰值 RSS、分配次数、重复遍历能力和取消后的资源回收。只有在一次顺序消费和内存约束确实重要时,惰性收益才值得引入协程复杂度。

高质量示范回答

我会选择 std::generator 的前提是结果量大、顺序消费一次,并且生产可以在每个元素处暂停。生成器调用先建立协程状态,迭代器恢复协程直到下一次 co_yield;因此消费者能先拿到第一个值,工作集不随完整结果线性增长。

我会明确 owner:源树、文件和字符串必须活得比生成器久,生成器内部的句柄和临时资源用 RAII 管理。需要随机访问、size 或重复遍历时返回 vector;已有范围上的纯变换使用 views;需要网络等待或跨线程时采用异步流抽象。最后用空值、提前 break、异常、深递归和极大输入基准比较首元素延迟、峰值内存、全量吞吐与资源回收,确认协程的收益覆盖了额外语义成本。

常见错误

把 generator 当作异步流

它是同步协程,不能自动等待网络;修正方法是使用异步运行时和明确的异步流接口。

返回局部对象的引用或 view

生成器暂停后局部对象的生命周期可能结束;修正方法是让 owner 覆盖整个消费过程,或按值 yield。

以为 break 会完成业务清理

提前停止只结束迭代,不代表外部事务已提交;修正方法是让资源 RAII 析构并显式处理取消语义。

只比较全量耗时

惰性范围的价值常在首元素延迟与峰值内存;修正方法是同时测首元素、RSS、分配次数和重复遍历。

追问及应对

追问一:生成器能否并行消费?

同一个输入生成器通常是单向状态机,不应让多个线程同时递增。要并行就先分片输入,或把生产任务拆成独立生成器并明确合并顺序。

追问二:如何递归遍历一棵很深的树?

可以用 elements_of 组合子生成器,但要测量协程帧和深度;如果深度不可控,显式栈更容易限制内存并处理取消。

追问三:消费者保存了一个元素引用怎么办?

必须声明引用有效期只到下一次推进或生成器销毁前,除非源对象由外部 owner 保持。需要长期保存就复制值或转移所有权。

追问四:C++20 项目如何处理没有 std::generator?

可以用项目内的 generator/输入迭代器封装或返回 view,但要明确其所有权、结束和异常契约;不能只把 C++23 语法改名就假设语义完整。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具