プロンプトとコンテキスト
注文サービスは、入力バリデーション、在庫確保、および決済処理を実行します。在庫切れ、決済拒否、および依存関係のタイムアウトは想定される失敗です。一方で、プログラミングエラー、不変条件の破綻、および回復不能なリソース障害には別個のパスが必要です。面接官は、C++23 の std::expected を使用した戻り値の型、合成、およびエラー記録の設計を求めています。
これはエラーコントラクトと合成可能なコードを評価するものです。std::expected<T, E> には値 T またはエラー E のいずれかが含まれます。リトライやロギング、副作用のロールバックを自動で行うわけではありません。まずエラードメインを定義し、次に呼び出し元がどのように結果を検査するかを示し、最後に例外と補償の境界を明示してください。
面接官が評価している点
- ビジネス上の失敗、依存関係の失敗、および不変条件の破綻を区別できているか。
has_value、value、error、unexpected、および[[nodiscard]]を正しく使用しているか。and_then、transform、およびor_elseを使用して、例外を投げない失敗を合成できるか。std::expectedをグローバルな例外の代替や単なる文字列エラーとして扱うことを避けているか。- エラーマッピング、冪等性、ロギング、および補償トランザクションを網羅しているか。
最初に明確にすべき質問
- コードベースは C++23 としてコンパイルされ、その標準ライブラリは対象の API を実装していますか?
- エラーはローカルで処理されますか、それともサービス境界を越えてシリアライズされますか?サービス間エラーには安定したコードが必要です。
- 関数がロック、ファイルハンドル、または予約を保持していますか?失敗を返す前に状態のクリーンアップと補償を明示してください。
- 注文ステップは冪等ですか?決済のリトライは、在庫を2回解放することとは異なります。
- どの診断フィールドを保持する必要がありますか?ユーザー向けメッセージと内部ログは混在させるべきではありません。
30秒の回答フレームワーク
「想定されるビジネス上の失敗を std::expected<T, OrderError> としてモデル化し、呼び出し元が明示的に処理できるようにします。結果を [[nodiscard]] でマークし、バリデーション、予約、および決済を and_then で合成し、一貫したマッピングとメトリクスのために or_else を使用します。例外は、不変条件の破綻、初期化の失敗、または安全に回復できない境界のために残します。すべての副作用には、冪等性キー、補償プラン、および機密情報を含まないロギングが必要です。」
詳細解説
エラードメインの定義
OrderError には、invalidrequest、outofstock、paymentdeclined、dependency_timeout など、呼び出し元が対処できるカテゴリと、管理された内部コンテキストを含める必要があります。ユーザー向けテキスト、スタックトレース、ベンダーペイロードを1つの文字列にまとめてはいけません。プログラミングエラーや不変条件の破綻が、暗黙のうちに通常のビジネス上の失敗になってはなりません。
値とエラーの型の選択
バリデーションは std::expected<ValidatedOrder, OrderError> を、在庫確保は std::expected<Reservation, OrderError> を、決済処理は std::expected<Receipt, OrderError> を返すことができます。エラー型はムーブ可能で適度に小さく、所有権が明示的であるように保ちます。呼び出し元が失敗を暗黙のうちに破棄できないように、結果を生成する関数に [[nodiscard]] を付与します。
明確な境界での明示的なチェックの使用
ステップが短い場合や、エラーごとに異なるアクションがある場合、明示的なチェックは監査が容易です:
[[nodiscard]] auto reserve(const ValidatedOrder& order)
-> std::expected<Reservation, OrderError>;
auto create_order(Input input) -> std::expected<Receipt, OrderError> {
auto valid = validate(std::move(input));
if (!valid) return std::unexpected(valid.error());
auto held = reserve(*valid);
if (!held) return std::unexpected(held.error());
return charge(*valid, *held);
}成功を確認した後にのみ operator* および value() を使用してください。失敗パスでは元のカテゴリを保持し、在庫切れを不透明な汎用エラーとして偽装しないでください。
モナド操作による同種チェーンの合成
すべてのステップが std::expected を返す場合、and_then は値が存在する場合のみ継続し、エラー時はショートサーキットします。transform は成功値をマップし、or_else はエラーを記録または変換します。また、C++23 は境界での変換用に transform_error も提供します。副作用の順序は可視状態を維持し、決済、リトライ、補償を監査不能なチェーンの中に隠さないでください。
例外境界の設定
タイムアウト、在庫切れ、決済拒否は想定される結果であるため、expected を使用することで、ビジネス層がリトライ、ユーザーフィードバック、または手動処理を選択できるようになります。不変条件の破綻、初期化の失敗、または安全に回復できない境界では例外を使用できます。境界の一貫性を保ちます。同じエラーカテゴリが同一の関数からある時は返され、ある時はスローされるようなことがあってはなりません。
副作用、冪等性、補償の処理
expected は結果を運ぶだけであり、完了した副作用を取り消すわけではありません。在庫確保が成功して決済が失敗した場合、在庫確保を解放するか補償状態に遷移します。決済のリトライには冪等性キーが必要です。エラーオブジェクトは機密情報を含めることなく注文 ID、ステップ、リトライのアドバイスを保持でき、ロギングは決済情報を保存することなくトレースを関連付けることができます。
コントラクトをテスト可能にする
各エラー分岐、ショートサーキット、マッピング、および補償の順序をテストします。予期しない例外が境界を越え、一貫して処理されることをテストします。CI でコンパイラ、標準ライブラリの機能テストマクロ、ビルドフラグを固定します。サービス間では、C++ の型名を API コントラクトとして公開する代わりに、OrderError を安定したプロトコルコードにマッピングします。
質の高い模範解答
「安定したカテゴリと管理された内部診断を備えた OrderError を定義します。バリデーション、在庫確保、および決済は [[nodiscard]] std::expected の値を返します。在庫切れ、決済拒否、およびタイムアウトは呼び出し元が明示的に処理するビジネス上の失敗であり、これらすべてを例外にするわけではありません。
短いチェーンの場合、if (!result) を使用して std::unexpected(result.error()) を伝播させ、各副作用の境界を監査可能な状態に保ちます。純粋な同種変換には and_then と transform を使用でき、or_else でエラーの記録とマッピングを行います。value() は成功が確認された後にのみ呼び出します。
在庫確保が成功して決済が失敗した場合、expected はロールバックを行わないため、冪等な状態を記録して解放または補償を実行します。例外は、不変条件の破綻や、現在の境界で安全に回復できない初期化の失敗のために予約されます。CI は C++23 ツールチェーンを固定し、エラーを安定したサービスコードにマッピングする前に、すべてのエラー、ショートサーキット、および補償パスをテストします。」
よくある間違い
std::expectedをリトライエンジンとして扱う: 戻り値の型にはリトライセマンティクスがありません → エラーカテゴリと冪等性を使用してビジネス層で決定します。std::optionalを使用して理由を失う: 呼び出し元が在庫の失敗とタイムアウトを区別できなくなります → 安定したエラー型を使用してください。[[nodiscard]]を無視する: 呼び出し元が失敗を破棄する可能性があります → 結果を返す関数にアノテーションを付け、CI で警告をエラーとして扱います。- チェックせずに
value()を呼び出す: 失敗時にbad_expected_accessがスローされる可能性があります → 明示的に分岐するか、事前にチェックしてください。 - すべての例外をエラーコードに変換する: 不変条件の破綻がもみ消される可能性があります → 明確な例外境界を維持してください。
- エラーオブジェクトに機密情報を含める: ログやシリアライゼーションによって認証情報が漏洩する可能性があります → ユーザー向けメッセージ、安定したコード、および内部診断を分離してください。
- 完了した副作用を無視する: 決済失敗後も在庫が確保されたままになります → 冪等な状態と補償を設計してください。
- C++ の型をサービス間で送信する: アップグレードによってプロトコルが破損します → バージョン管理された安定したコードとフィールドにマッピングしてください。
フォローアップの質問と回答
フォローアップ 1: std::expected と例外のどちらをどのように選びますか?
呼び出し元が処理できる想定内のビジネス結果には expected を使用します。不変条件の破綻、初期化の失敗、または安全に回復できない境界には例外を使用します。普遍的なルールよりも一貫性が重要です。呼び出し元が1つのエラーカテゴリに対して制御フローを推測するようなことがあってはなりません。
フォローアップ 2: なぜ std::optional<T> ではないのですか?
optional は存在または非存在を表しますが、理由を保持しません。注文フローでは在庫、決済、依存関係の失敗に対して異なるアクションが必要となるため、expected<T, E> が必要です。
フォローアップ 3: 決済処理を and_then の内部で実行できますか?
副作用の境界が可視のままであり、各ステップがリトライ可能または補償可能であれば実行できます。異なるポリシーを持つ長いチェーンの場合、明示的な if の方が監査しやすい場合があります。関数型風のスタイルのために状態変化を隠さないでください。
フォローアップ 4: 複数の依存関係からのエラーをどのように統合しますか?
サービス境界で安定したドメインエラーを定義します。内部原因を保持しながら、ベンダーの失敗を有限のカテゴリセットにマッピングします。マッピングには transform_error または or_else を使用し、ベンダーコードとトレースを内部的に記録し、ベンダー独自の文言をユーザーメッセージから排除します。
フォローアップ 5: エラーを返す方が例外をスローするよりも遅いですか?
実際のパスを測定してください。expected は一般的な失敗を明示的にしますが、エラーオブジェクトのコピーや分岐の追加が発生する場合があります。例外はスローパスにコストを集中させます。絶対的な主張ではなく、レイテンシ目標、コンパイラの動作、およびエラー頻度のベンチマークに基づいて選択してください。
フォローアップ 6: 呼び出し元がチェックを忘れるのをどのように防ぎますか?
結果の型および重要な関数に [[nodiscard]] を付与し、コンパイラの警告を CI の失敗として扱い、すべての value() 呼び出しをレビューします。言語をまたぐ API については、コンパイラが強制できない部分をカバーするためにプロトコル状態テストとコントラクトテストを追加します。