具代表性的面試主題

C++23 面試:std::generator 如何實作惰性範圍?

程式題困難
Offer.cc 編輯團隊發佈 更新

題幹

請用 std::generator 產生惰性序列,並說明它何時比回傳 vector、回呼或 views 更合適,以及如何避免懸空參照和資源洩漏。

題幹與適用場景

你需要遍歷一個可能很大的樹或檔案流,只在消費者要求下一個元素時產生值,不能一次把全部結果放入記憶體。請使用 C++23 std::generator 表達惰性範圍,解釋 co_yield 的執行模型、例外和物件生命週期,並比較其他實作方式。

這是一道 coding 題,重點是協程控制代碼、輸入範圍語意和資源邊界。假設消費者只做單執行緒、單向遍歷,生成器參照的外部物件在遍歷完成前保持有效。

面試官考察點

  • 能否區分惰性產生、容器實體化和普通 views 的職責。
  • 是否理解第一次迭代、每次 ++、結束和銷毀分別發生什麼。
  • 是否識別生成器參照區域變數、暫時物件和非同步資源的生命週期風險。
  • 能否說明例外傳播、取消遍歷和遞迴 elements_of 的代價。
  • 是否以資料量、首元素延遲、峰值記憶體和重複遍歷需求作出選擇。

回答前需要釐清的問題

  1. 消費者只遍歷一次還是需要多次重用?一次遍歷更適合生成器,重用可能需要容器。
  2. 元素是值、參照還是 view?參照可減少拷貝,但要求來源物件活得更久。
  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 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具