契約による設計(Design by Contract)の実装を検討 - torutkのブログで契約による設計の実装を検討しました。今回は、検討に従って実際にC++でプログラムコードに表してみます。
事前条件
C言語標準マクロ assert を用いた実装
C言語の標準マクロassertを使った事前条件検出と通知のコード例です。コンパイル時にNDEBUGシンボルを定義しなければ、事前条件違反時にプログラムを停止させます。
void Person::setAge(int age) { assert(0<=age && age<=200); // 年齢有効範囲は0才〜200才 ... 処理 }
C++例外機構を用いた実装
例外機構で実装すると、検出箇所(ファイル名・行番号)やメッセージを例外クラス生成時に指定する必要があります。また条件判定に普通にC++の条件文で記述するため少しコードが煩雑です。
- 検出箇所を保持する例外クラス
class PreConditionException : public std::exception { public: PreConditionException(const char* file, int line, const char* message); virtual ~PreConditionException() throw(); virtual const char* what() const throw(); };
- 事前条件を制御構文(以下の例ではif文)で記述し、事前条件違反を検出するとファイル名・行番号と条件式文字列を引数に渡します。
void Person::setAge(int age) { if (age < 0 || 200 < age) { throw PreConditionException(__FILE__, __LINE__, "0<=age<=200"); } ... 処理 }
自前でマクロと例外を組み合わせた実装
契約による設計のための自前のマクロDBC_REQUIREと、契約による設計の事前条件例外PreConditionExceptionを以下のように定義します。マクロはコンパイル時に取り除けるよう#ifdef DBCで制御します。例外は
#ifdef DBC #define DBC_REQUIRE(condition) \ if (!(condition)) throw PreConditionException(__FILE__, __LINE__, #condition) #else # define DBC_REQUIRE(condition) #endif
上記のマクロと例外を使用して、事前条件の表明を記述します。ずいぶんすっきりします。
void Person::setAge(int age) { DBC_REQUIRE(0<=age && age<=200); ... 処理 }
事後条件
C言語標準マクロ assert を用いた実装
void Person::setAge(int age) { ... 処理 assert(age_ == age); // 事後条件は引数で指定した年齢がメンバー変数age_と一致 return; }
C++例外機構を用いた実装
void Person::setAge(age) { ... 処理 if (age_ != age) { throw PostConditionException(__FILE__, __LINE__, "age_!=age"); } return; }
自前でマクロと例外を組み合わせた実装
#ifdef DBC #define DBC_ENSURE(condition) \ if (!(condition)) throw PostConditionException(__FILE__, __LINE__, #condition) #else # define DBC_ENSURE(condition) #endif
void Person::setAge(int age) { ... 処理 DBC_ENSURE(age_ == age); return; }
クラス不変条件
クラス不変条件は、クラスのメンバ関数として定義し、メンバ関数の事前条件判定時およびメンバ関数終了時(途中で例外や復帰での終了も含まれる)にチェックします。チェック箇所が多いので、抜けがないようRAIIイディオムを使います。
クラス不変条件のメンバ関数定義
RAIIで不変条件をチェックするクラスを以下に定義します。汎用化するために、テンプレートを用いています。
template <typename T> class Invariant { public: Invariant(const T& target) : target_(target) { target_.assertInvariant(); } ~Invariant() { target_.assertInvariant(); } private: const T& target_; };
クラス不変条件を設けるクラスは、上記RAIIでチェックするためassertInvariant()メンバ関数を定義します。
このメンバ関数の中では、C言語マクロassertを使用するなり、例外を使用するなり、独自マクロを使用するなり、随意にクラス不変条件の表明を記述します。
以下はassertを使用したクラス不変条件の表明の例です。
class Person { ... void assertInvariant() const; }; void Person::assertInvariant() const { assert(name_ != 0 && id != 0); }
あとは、各メンバ関数で不変条件チェックを行います。
void Person::setAge(int age) { Invariant<Person>(*this) inv; // RAIIによるクラス不変条件実行の仕込み DBC_REQUIRE(0<=age && age<=200); // 事前条件表明 age_ = age; DBC_ENSURE(age_ == age); // 事後条件表明 return; // 復帰時にRAIIのクラス不変条件チェックが実行 }
課題・応用
- 継承関係にあるクラスでは、基底クラスの事前条件・事後条件・クラス不変条件も加味するべきです。
- 事後条件をRAIIで実現するときは、契約による設計の対象メンバ関数1つにつき1つの事後条件判定関数を作成するのは手間です。