プロンプトとスコープ
注文ライブラリを所有しており、API の事前条件、事後条件、および関数本体の不変条件を表現するために C++26 Contracts を導入したいと考えています。pre、post、および contract_assert のセマンティクスの境界、評価モード、違反処理、ならびに信頼できない入力のバリデーションを落とさない既存の assert 利用からの移行パスを説明してください。コンパイラサポートが不完全な状況下でのリリース戦略も含めてください。
面接官が見ているポイント
- コントラクトドキュメント、実行時診断、およびビジネス入力バリデーションの明確な分離。
- 述語が省略されたり複数回評価されたりする可能性があるため、副作用を持ってはならない理由の説明。
- すべての違反が例外をスローすると仮定せずに、違反ハンドラ、テレメトリ、および障害の分離を設計すること。
- コンパイラ機能マトリックスと段階的ロールアウトを活用して、C++26 のサポート状況の差異を管理すること。
明確化のための質問
- 呼び出し元は信頼できるライブラリコードですか、それとも直接のネットワークリクエストハンドラですか?
- 本番環境では違反を監視(observe)すべきですか、プロセスを終了(terminate)すべきですか、それともリクエストを分離して継続すべきですか?
- 対象となるコンパイラ、標準ライブラリのバージョン、および ABI のリリースサイクルは何ですか?
30秒の回答フレームワーク
責任の観点から始めます。pre は呼び出し元が満たすべき条件を示し、post は呼び出し先がリターン時に保証する内容を示し、contract_assert は関数本体のローカルなコントラクトを示します。次に、実装によって評価が省略されたり繰り返されたりする可能性があるため、述語には副作用があってはならないことを説明します。違反はハンドラおよびデプロイメントポリシーを通じて処理されます。最後に、信頼できない入力に対しては明示的なバリデーションを維持し、移行には機能検出、コンパイラマトリックス、およびカナリアリリースを活用します。
ディープダイブ回答
1. コントラクトレイヤーの確立
事前条件は呼び出し元の責任であり、事後条件は呼び出し先の責任です。これらは、正のキャパシティや順序保証といった合成可能な API 制約を表現します。関数本体の contract_assert は、ローカルな不変条件やアルゴリズム段階の前提を表現します。これら3つはすべてコードレビューで読み取りやすく、リカバリポリシーとは分離しておく必要があります。
2. 述語を副作用のない状態に保つ
述語は状態を読み取ってブール値を計算することのみを行うべきです。カウンタのインクリメント、キャッシュの変更、リソースの解放、またはその場限りのランダム性への依存を含めてはなりません。C++26 のコントラクトセマンティクスではさまざまな評価モードが許可されており、評価を省略するものもあれば、複数回評価するものもあります。副作用が存在すると、正しいプログラムの挙動がビルド構成に依存することになってしまいます。
int withdraw(Account& a, int amount)
pre (amount > 0)
pre (amount <= a.balance())
post (a.balance() == old_balance - amount);
{
contract_assert(a.is_open());
return a.debit(amount);
}スニペット内の old_balance は設計上の課題を浮き彫りにしています。C++26 は一般的な事後条件キャプチャを提供しないため、old(...) 構文が存在すると仮定してはなりません。古い値が必要な場合は、関数内で明示的に保存し、その保存によってビジネスセマンティクスが変化しないことを確認するか、将来の標準拡張を待つ必要があります。
3. 評価および違反処理の選択
observe や enforce などのモードに対する動作を定義します。開発やテストでは診断情報を収集し、クリティカルなサービスでは enforce のもとでフェイルファストさせ、リクエスト境界では失敗を監視可能で分離された結果に変換できます。違反が常に例外をスローすると約束してはなりません。動作は実装やビルド構成に依存します。ハンドラはコントラクトの位置、リクエストの相関 ID、およびバージョンを記録し、同一コントラクトパスへの再帰的侵入を回避する必要があります。
4. 境界での入力バリデーションの維持
ネットワークフィールド、ユーザー金額、および権限は信頼できません。内部でコントラクトが設定された関数を呼び出す前に、これらを明示的にバリデーションし、期待されるビジネスエラーを返してください。コントラクトは信頼できる呼び出し元同士のプログラマによるミスを捕捉できますが、認証、認可、レート制限、またはフォーマットチェックを代替するものではなく、データクレンジングの唯一の防壁にしてはなりません。
5. 移行とリリースの計画
機能マクロとコンパイラバージョンごとに機能マトリックスを構築し、構文、ハンドラ、デバッグ情報、最適化ビルドを個別にテストします。テストおよびカナリア環境で observe モードを有効化し、違反率とオーバーヘッドを比較した上で、徐々に enforce を引き上げます。従来の assert にはマクロ展開、NDEBUG、および副作用に関する前提があり、機械的に等価とみなすことはできません。各利用箇所を監査し、障害セマンティクスを維持し、統一されたレポートが必要な場合はアダプタを使用してください。
質の高い模範解答
私は Contracts を、入力バリデーションや例外システムではなく、実行可能な設計制約として扱います。pre は呼び出し元を制約し、post は呼び出し先を制約し、contract_assert は関数本体の不変条件を制約します。実装が評価を省略したり繰り返したりする可能性があるため、すべての述語は読み取り専用に保ち、I/O、カウンタの変更、リソースの解放を禁止します。違反は構造化された診断情報を出力する1つのハンドラを経由します。その後、ビルドポリシーによって observe、enforce、またはフェイルファストの動作が選択され、コードは例外が発生することを前提としません。リクエスト境界では依然として認証、認可、および信頼できないデータのチェックを実施します。移行にあたっては、コンパイラ機能マトリックスを作成し、テストとカナリアから開始して徐々に enforce を高めます。assert を機械的に置き換えるのではなく、NDEBUG やマクロの副作用を監査します。これにより、オンラインのリカバリが一様でない実装の挙動に依存することなく、安定した内部チェックを提供できます。
よくある間違い
preをすべての入力に対する完全なセキュリティチェックとして扱うこと。- 述語の内部でログ記録、メトリクスのインクリメント、またはオブジェクトの変更を行うこと。
- モードに関係なく、違反は必ず例外をスローする、または必ず終了すると主張すること。
- すべての
assertをcontract_assertに置き換え、NDEBUG、マクロ引数、または副作用を見落とすこと。 - 存在しない一般的な
old(...)構文を用いて C++26 の事後条件を説明すること。
フォローアップ質問と回答
面接官が「なぜすべてに例外を使わないのか?」と尋ねてきたら?
コントラクトは呼び出し元と呼び出し先の責任を明記し、レビューやビルドポリシーに関与できます。一方、例外は制御フローとリカバリを定義します。これらは互いに補完し合うものであり、交換可能ではありません。
述語の反復評価のコストが高すぎる場合は?
述語を読み取り専用かつ軽量に保ち、各ビルドモードで測定します。コストのかかる診断はコントラクト内に副作用として隠すのではなく、明示的なパスに配置します。
仮想関数が直接コントラクトを持てるかどうか尋ねられたら?
C++26 にはこの点に境界があります。WG21 のロードマップでは、仮想関数のサポートは将来の拡張として挙げられています。将来の提案を既存の C++26 の動作として提示するのではなく、対象のコンパイラと標準バージョンを確認してください。