HN 日本語サマリー

← 一覧へ戻る
プログラミング

C++26 Contracts: 前提条件と事後条件の紹介

C++26 Contracts: Introduction to Preconditions and Postconditions (cppstories.com)

45 pointsby ibobev35 コメント

要約

C++26で導入されるContracts機能について解説しています。assert文との違いや、関数宣言に前提条件(pre)や事後条件(post)を記述する方法、コンパイラによる評価セマンティクス、GCCでのコンパイル方法などが具体例と共に示されています。これにより、APIの堅牢性と可読性が向上します。

全文翻訳

最終更新: 2026年10月11日 C++26 Contracts: 前提条件と事後条件の紹介 目次 Standard Libraryの堅牢化に関する記事では、無効なStandard Libraryコンテナへのアクセスやその他の壊れた前提条件に対するチェックを見てきました。それらの要件は「契約(contracts)」という言葉で表現されると述べました。では、私たちのコードはどうでしょうか?この記事では、C++26のContractsを探求します。簡単なcassertから始め、要件を関数自体に移動させ、結果をチェックする方法を見ていきます。宣言と定義、コンパイラオプション、そして少し驚くかもしれない詳細についても触れます。 Assertionから始める 小さな例から始めましょう。まずヘッダーファイルです。 // foo.h #include <vector> int get_value(const std::vector<int>& values, std::size_t index); そして実装です。 // foo.cpp #include "foo.h" // 私たちの関数の宣言... #include <cassert> int get_value(const std::vector<int>& values, std::size_t index) { assert(index < values.size()); return values[index]; } この関数は有効なインデックスを期待します。条件が偽の場合、assertは診断メッセージを表示してプログラムを終了します。しかし、これはアサーションが有効な場合にのみ発生します。NDEBUGを定義するとチェックが無効になり、基本的にコードからチェックが削除されます。 この単純なアプローチの問題点は何でしょうか?要件は実装の中にあります。宣言だけを読む人は、ベクトルとインデックスを見ますが、それらの関係性は分かりません。 前提条件を追加する C++26 Contractsを使用すると、関数本体内のアサーションからその要件を関数自体に移動させることができます。 int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()) { return values[index]; } パラメータリストの後に新しいpre(...)構文があることに注意してください。これは前提条件アサーションを導入します。これは関数に入るときに真であると期待される条件です。さらに、この前提条件を宣言に追加することもできます。 int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()); この機能は、P2900、Contracts for C++ を通じて導入されました。それがどのように機能し、assertとどう違うのかを理解しましょう。 なぜassertの代わりにContractsを使うのか? 最初は、pre(index < values.size()) は assert(index < values.size()) を書く別の方法のように見えるかもしれません。どちらも真であるべき条件を表現します。では、Contractsを使用する利点は何でしょうか?いくつかの重要な違いがあります。 宣言における要件: preとpostを関数の宣言に直接配置できます。これにより、実装を見なくても、APIを読む人全員に要件が表示されます。 より柔軟なチェック: 従来のassertはNDEBUGを通じて有効または無効になります。Contractsは、チェックを無視する、違反を報告して続行する、プログラムを終了するなど、いくつかの評価セマンティクスを定義します。コンパイラの実装は、どのセマンティクスが利用可能で、どのように選択できるかを決定します。 前提条件と事後条件: Contractsは、呼び出し元が提供しなければならないものと、関数が返すと約束するものとを区別します。assertを使用する場合、関数本体内に手動でチェックを配置する必要があり、異なるreturn文の前にチェックを含める必要がありました。これは、実行時チェックが無効な場合でも、ContractsがAPIを文書化するのに役立ちます。 完全な前提条件の例 ここでは完全な例を示します(簡単のため、現時点ではヘッダーファイルはありません)。 #include <print> #include <vector> int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()) { return values[index]; } int main() { std::vector<int> values { 10, 20, 30 }; std::println("{}", get_value(values, 1)); // get_value(values, 10); // 前提条件違反 } 有効な呼び出しは次のように表示されます。 20 そして、問題のある呼び出しをコメント解除して試すためのバージョンがこちらです。 Compiler Explorerで見る GCCでは、次のような結果になります。 プログラムの戻り値: 134 プログラム標準出力 20 プログラム標準エラー contract violation in function int get_value(const std::vector<int>&, std::size_t) at /app/example.cpp:6: index < values.size() [assertion_kind: pre, semantic: enforce, mode: predicate_false, terminating: yes] terminate called without an active exception Program terminated with signal SIGABRT (6) 宣言と定義におけるContracts 明確にするために、Contractsは宣言、定義、またはその両方に表示できます。 int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()); その後、定義は契約指定子を省略できます。 int get_value(const std::vector<int>& values, std::size_t index) { return values[index]; } 契約アサーションは、関数の最初の宣言によって確立されます。後続の宣言または定義は、上記のよう契約指定子を省略するか、同じシーケンスを繰り返すことができます。後続の宣言は、異なる前提条件または事後条件を導入することはできません。実際には、関数がヘッダーファイルで宣言されている場合、その最初の宣言が前提条件と事後条件の自然な場所となります。APIを読んでいる場合、実装を見なくてもすべての前提条件を確認するのが最善でしょう。さらに、Contractsは関数に関連付けられますが、関数の型の一部ではありません。 契約評価セマンティクス C++26は4つの評価セマンティクスを定義しています。 セマンティクス | 実行時に何が起こるか ---|--- Ignore | 述語は評価されません。 Observe | 違反が発生するとハンドラが呼び出されます。ハンドラが正常に返された場合は実行が継続されます。 Enforce | 違反が発生するとハンドラが呼び出されます。ハンドラが正常に返された場合は実行が終了します。 Quick-enforce | 違反が発生するとハンドラを呼び出さずに実行が終了します。 実装はどのセマンティクスが適用されるかを決定します。C++26には、個々の通常の契約アサーションに特定のポリシーを強制するためのポータブルな構文はありません。これらのルールは契約評価仕様に記載されています。 Ignoreセマンティクスでは、実行時チェックは行われません。Observeでは、違反が報告されますが、ハンドラが正常に返された場合は実行が継続されます。私たちの例では、どちらのポリシーも、無効なインデックスでプログラムがvalues[index]に到達することを許可する可能性があり、未定義の動作を引き起こします。したがって、pre(...)を追加することは要件を表現しますが、選択されたポリシーがチェックにどのように応答するかを決定します。 GCCでのコンパイル 2026年10月現在、GCC 16のみがContractsをサポートしています。特別なコンパイルフラグで使用できます。 g++ -std=c++26 -fcontracts \ -fcontract-evaluation-semantic=enforce example.cpp そのポリシーでは、無効な呼び出しは違反ハンドラを呼び出します。ハンドラが正常に返された場合、関数本体が無効なアクセスを実行する前に実行が終了します。GCCのデフォルトハンドラは診断情報を出力します。GCCオプションのドキュメントを参照してください - contracts。 GCCでは、デフォルトの契約違反ハンドラを置き換えることもできます。例えば、違反が検出されたときにカスタムメッセージをログに記録できます。ハンドラは、失敗した契約、場所、評価セマンティクスに関する情報を提供するstd::contracts::contract_violationオブジェクトを受け取ります。 #include <contracts> #include <print> void handle_contract_violation( const std::contracts::contract_violation& violation) { std::println(stderr, "Contract violated: {}", violation.comment()); } Compiler Explorerでの例を見る ハンドラを置き換えても、その後に何が起こるかは変わりません。Observeでは、実行は継続されます。Enforceでは、プログラムは終了します。すべての実装がハンドラを置き換えることをサポートしているわけではないため、この機能は実装定義です。 事後条件を追加する これまでは、呼び出し元が提供しなければならないものについて説明しました。関数が正常に返されたときに約束することも説明できます。この例を見てください。 int clamp_value(int value, const int low, const int high) pre(low <= high) post(result: result >= low && result <= high) { if (value < low) return low; if (value > high) return high; return value; } ここでは、2つの有用な要素があります。