課題と適用範囲
非常に巨大な可能性のあるツリーやファイルストリームをトラバースし、コンシューマが次の要素を要求したときにのみ値を生成する必要があります。すべての結果をメモリ上に実体化することはできません。C++23のstd::generatorを遅延範囲(lazy range)として使用し、co_yield、例外、生存期間について説明した上で、他の代替案と比較してください。
このコーディング課題は、コルーチンハンドル、入力範囲(input-range)セマンティクス、およびリソース境界に焦点を当てています。シングルスレッド、1回の前方トラバース、および参照される外部オブジェクトがトラバースよりも長く生存することを前提とします。
面接官が評価するポイント
- 遅延生成、実体化されたコンテナ、および通常の
viewsを区別できているか。 - 初回イテレーション時、各
++時、完了時、および破棄時に何が起こるかを理解しているか。 - ローカル変数、一時オブジェクト、参照、非同期リソースに対する生存期間のリスクを把握できているか。
- 例外の伝播、早期終了、および再帰的な
elements_ofのコストを説明できるか。 - データサイズ、最初の要素のレイテンシ、ピークメモリ、再利用の必要性に基づいて選択を行っているか。
回答前に確認すべき質問
- コンシューマはシングルパスですか、それとも再利用が必要ですか? シングルパスならジェネレータが適しており、再利用ならコンテナが適している場合があります。
- 要素は値、参照、またはviewですか? 参照はコピーを回避しますが、生存期間の要件が長くなります。
- 生成処理はI/Oでブロックするか、イベントを待機するか、スレッドをまたぎますか?
std::generatorは同期処理であり、非同期ワークをスケジュールしません。 - ランダムアクセス、
size、または並列アルゴリズムが必要ですか? 入力範囲は通常これらを提供しません。 - トラバースが早期終了した場合はどうなりますか? ファイルハンドル、ロック、バッファには明示的な所有者とクリーンアップパスが必要です。
30秒の回答フレームワーク
ジェネレータを同期的で前方向の入力範囲としてモデル化します。co_yieldは各要素で中断(サスペンド)し、コンシューマを進めると次のyield、return、または例外が発生するまでコルーチンが再開されます。これは、1回だけ消費され、最初の要素を迅速に生成する必要がある大規模な結果に適しています。ランダムアクセス、反復トラバース、またはスレッド間の非同期I/Oが必要な場合は、ソースとリソースの生存期間を確認した上で、vector、viewパイプライン、または非同期ストリームを選択します。
ステップごとの詳細解説
1. 範囲と所有権の確立
std::generator<T>はC++23の同期コルーチン範囲です。ジェネレータ関数を呼び出すと、一般にコルーチン状態が作成され、イテレーション中に実行が開始されます。ジェネレータはそのフレームを所有し、イテレーションが終了するかジェネレータが破棄されたときに解放されます。ローカルコンテナへの参照を決して返してはいけません。呼び出し元または外部の所有者が参照先オブジェクトの生存を維持する必要があります。
2. co_yieldでワーキングセットを一定に保つ
#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;
}
}このコードは最初に100万個の要素を構築するわけではありません。各再開によって次のco_yieldまで進みます。breakによってイテレータとジェネレータが破棄されるため、コルーチンフレームを再利用することはできません。選択した標準ライブラリおよびコンパイラバージョンに対するC++23のサポート状況を確認してください。
3. 4つの実装の比較
vectorを返すのは最もシンプルで、サイズ、ランダムアクセス、再利用をサポートしますが、すべてを実体化します。コールバックはプロデューサ側に制御を持たせますが、範囲アダプタとの合成が困難です。手書きの入力イテレータはC++23以前でも機能しますが、状態、終端、例外のルールを自前で維持する必要があります。std::viewsは既存の範囲に対するステートレスな変換に適しています。ジェネレータはステートマシン、再帰トラバース、または要求(プル)されたときにのみ進めるべきロジックに適しています。
4. 再帰と参照の処理
ツリートラバースでは、ネストされたループの代わりにelements_ofを使用して子ジェネレータを合成できますが、深さ、コルーチンフレーム数、および例外パスを測定してください。std::string_viewやノード参照をyieldする場合、トラバース全体を通してソース文字列とノードが有効であり続ける必要があります。一時文字列へのviewをyieldしたり、ソース所有者の生存期間を超えてジェネレータを保持したりしてはいけません。
5. 例外、早期終了、リソースの処理
ジェネレータ内の例外は、イテレータが再開されたときにコンシューマに到達します。ログ記録、再試行、停止のいずれを行うかはコンシューマが決定します。breakはビジネスレベルのコミットではありません。ファイルハンドル、ロック、一時バッファはジェネレータフレーム内のRAIIオブジェクトとし、破棄時に解放されるべきです。これは同期処理であり、co_yieldはネットワークを待機したり、ブロッキングI/Oを非同期ワークに変換したりしません。
6. 境界ベンチマークでまとめる
空の範囲、1要素の範囲、巨大な範囲、再帰の深さ、例外による中断、無効化された参照をテストします。最初の要素のレイテンシ、全体の実行時間、ピークRSS、メモリ割り当て、再現性、キャンセル後のクリーンアップについて、vector、ジェネレータ、viewパイプラインを比較します。1回限りの消費とメモリ制約によって遅延評価の利点が明確になる場合にのみ、コルーチンの複雑さを導入してください。
高品質な模範回答
結果が大きく、順番に1回だけ消費され、各要素の後に生成を一時停止できる場合にはstd::generatorを選択します。ジェネレータを呼び出すとコルーチン状態が作成され、イテレータが次のco_yieldまでそれを再開するため、コンシューマは結果全体を実体化することなく最初の値を早期に受け取ることができます。
私は所有権を明確にします。ソースツリー、ファイル、文字列はジェネレータよりも長く生存させ、コルーチンハンドルと一時リソースにはRAIIを使用します。ランダムアクセス、サイズ取得、反復トラバースにはvectorを返し、既存の範囲に対する純粋な変換にはviewsを使用し、ネットワーク待機やスレッドをまたぐ作業には非同期ストリーム抽象化を使用します。空の入力、早期break、例外、深い再帰、巨大な入力をベンチマークし、最初の要素のレイテンシ、ピークメモリ、スループット、クリーンアップを比較した上で、セマンティクス上のコストを受け入れます。
よくある間違い
ジェネレータを非同期ストリームとして扱う
同期処理であり、ネットワークをawaitすることはできません。代わりに非同期ランタイムと明示的な非同期ストリームインターフェースを使用してください。
ローカル変数への参照やviewを返す
コルーチンが中断されている間にローカル変数が破棄される可能性があります。外部の所有者がトラバース全体をカバーできるようにするか、値をyieldしてください。
breakが業務上のクリーンアップを完了すると仮定する
早期終了はイテレーションを終了させるだけであり、外部のトランザクションを完了させるわけではありません。RAIIクリーンアップと明示的なキャンセルセマンティクスを使用してください。
全体の実行時間のみを測定する
遅延範囲は、最初の要素のレイテンシやピークメモリの面で有利になる場合があります。最初の要素までの時間、RSS、割り当て、再現性も測定してください。
フォローアップ質問と回答
フォローアップ1:ジェネレータを並列に消費できますか?
1つの入力ジェネレータは通常、単一方向のステートマシンであり、複数のスレッドからインクリメントすべきではありません。入力を分割するか、独立したジェネレータを作成してマージ順序を定義してください。
フォローアップ2:非常に深いツリーをどのようにトラバースしますか?
elements_ofを使用して子ジェネレータを合成しますが、フレーム数と深さを測定してください。深さが制限されていない場合は、明示的なスタックを使用することでメモリ制限とキャンセルがより明確になります。
フォローアップ3:コンシューマが要素の参照を保存した場合はどうなりますか?
外部の所有者がソースの生存を維持していない限り、次のインクリメントまたはジェネレータの破棄までの有効性を文書化(仕様化)してください。長期保存する場合は値をコピーするか所有権を移転します。
フォローアップ4:C++20プロジェクトにstd::generatorがない場合はどうしますか?
プロジェクト独自のジェネレータや入力イテレータラッパーを使用するか、viewを返します。ただし、所有権、終端、例外の規約を明確に規定してください。C++23の構文名を変更しただけでは、そのセマンティクスを再現したことにはなりません。