C++における契約プログラミング♪
契約プログラミング
ある程度の規模のプログラムを書くと、関数が増えてきます。ある関数に何らかのデータを渡し、処理して結果を返したり、あるいはクラスの状態が変わったりします。ところで、プログラム中の実行時エラーには2種類あります。
- プログラマのミスによる実行時エラー (誤った計算によるバグ、メモリのバグ、想定しない状態によるバグ)
- ユーザーのシステムによる実行時エラー (リソース不足、ユーザーの誤った入力)
通常、前者はデバッグ中にのみ検出を行い、リリース版では後者のみ判断を行います。全てのデバッグが終わり、全てのプログラムがプログラマの意図するとおりに動くことを確認したらリリース版で動かすのです。
契約プログラミングによる設計では、このようなプログラマのミスによる実行時エラーを検出するために、プログラムソース中にそのような条件を記述することにより、デバッグ時に確実に条件を守らせる = 最低限のテストを盛り込むことができます。
契約プログラミングにおける条件は、
- 事前条件 : 関数の開始時に満たすべき条件
- 事後条件 : 関数の終了時に満たすべき条件
- 不変条件 : 外部に公開されるオブジェクトが常に満たすべき条件
があります。
assertマクロを使えば、デバッグ時に有効な条件を記述でき、リリース時には消し去ることが出来ます。ところがこのマクロだけだと、事後条件や不変条件は書きづらくなり、見た目にもわかりにくくなります。そこで独自のマクロとクラスを定義することで契約プログラミングがしやすくなるようにしました。
assertマクロ
ある関数funcはint型の引数1つを受け取ってint型の値を返すとします。そしてこの関数の引数には0以上の整数が与えられていると仮定しています。更に内部の計算によって出力は常に0以上となると仮定します。
int func(int n) { // nは0以上の時のみ動作する関数である // ~~処理~~ // 処理した後の結果retは常に0以上になるはずである return ret; }
この関数を呼び出す側は常に0以上で呼び出す必要があるわけです。このようなチェックは通常関数内には書きません。なぜならば、呼び出す側が0以上と保証している場合、無駄な処理(if文)が走ってしまうからです。このような前提を事前条件と言い、関数の責任ではなく呼び出し側の責任となります。
とはいえ、事前条件は文字で書くだけだとデバッグ時に誤って0未満の数を渡してしまう可能性があります。(呼び出し側が常に確認しているとは限りません。なぜならば、もしも理想のプログラムを書いたときには事前条件を省略できる可能性があるからです。) 契約プログラミングでは、コメントではなく、プログラム中に条件を書きます。
例えばassertマクロがあります。これはデバッグ時のみ有効で、
assert(n >= 0);
と書いて、常にn >= 0となるはずだ、と表明しているわけです。
提案手法
使い方
例えば次のような意味の無いクラスTestがあったとします。
class Test { public: // nは0以上 Test(int n) : n { n } { } // mは0以上 void test(int m) { --m; n -= m; } private: int n; // nは常に0以上 };
このとき、コンストラクタは、
- 事前条件 : n >= 0
そしてtestメンバ関数は、
- 事前条件 : m >= 0
- 事後条件 : m >= 0
- 不変条件 : n >= 0
です。これらをプログラム中に書くマクロを定義した上で、次のように書きます。
class Test { public: Test(int n) : n { n } { CREQUIRE(n >= 0); // 事前条件 } // 不変条件 CINVARIANT({ CREQUIRE(n >= 0); }); void test(int m) { // 事前条件 CREQUIRE(m >= 0); // 事後条件 CENSURE({ CREQUIRE(m >= 0); CCHECK(); // 不変条件のチェック }); --m; n -= m; } private: int n; };
実装
- contract.h
#pragma once #include <cassert> #include <string> #define CREQUIRE(condition) assert(condition, #condition); #ifdef NDEBUG #define CENSURE(condition) (nullptr) #else #define CENSURE(condition) auto _debug_contract_ = Contract::makePostCondition([&](){ condition }); #endif #ifdef NDEBUG #define CINVARIANT(condition) (nullptr) #define CCHECK() (nullptr) #else #define CINVARIANT(condition) void _ContractCheckInvariant() const { condition; } #define CCHECK() (this->_ContractCheckInvariant()) #endif namespace Contract { template <typename F> class PostCondition { public: PostCondition(F condition) : condition { condition } { } ~PostCondition() { condition(); } F condition; }; template <typename F> static PostCondition<F> makePostCondition(F&& condition) { return PostCondition<F> { std::forward<F>(condition) }; } }