Dの契約プログラミング vs C++
多くの人が、D の契約プログラミング(DbC) は既に C++ でできる以上のことを何もつけ加えていない、と私に書き送ってくれました。 そして彼らは、 C++ で DbC を行う技術を示してそれを説明しようとします。DbC についてもう一度考え直して、D でどのように実現されているか、 そしてそれぞれC++の様々なDbCテクニックと比較していく、 というのは意味のあることでしょう。
Digital Mars C++ は C++ への拡張 としてDbCをサポートしていますが、ここではこれについては触れません。 C++の標準ではなく、他のC++コンパイラでは全くサポートされていないからです。
D での 契約プログラミング
これは D の 契約プログラミング という文書により詳しく説明されています。まとめると、 D での DbC は次のような特徴を持ちます:- assert が基本的な"契約"になります。
- assert契約が失敗したときには、例外が送出されます。 このような例外は捕捉して処理することも、 そのままプログラムを終了させることもできます。
- クラスは クラス不変条件 を持つことができて、 public メンバ関数の開始時と終了時、 コンストラクタの終了時とデストラクタの開始時に毎回チェックされます。
- オブジェクトへの参照に対する assert契約は、 そのオブジェクトのクラス不変条件を検査します。
- クラス不変条件は継承されます。つまり、 派生クラスの不変条件は暗黙の内に基底クラスの不変条件を呼び出します。
- 関数は 事前条件 と 事後条件 を持てます。
- クラスの階層構造の中では、 派生クラスのメンバ関数の事前条件は、 オーバーライドする全ての関数の事前条件とORされます。 事後条件はANDされます。
- コンパイラのスイッチを切り替えることで、DbCのコードを有効にしたり コンパイル後のコードから取り除いたりすることができます。
- DbCチェックが有効であってもそうでなくても、 コードは意味的に同等に振る舞います。
C++ での 契約による設計
assert マクロ
C++ は実際、基本である assert マクロを備えていて、 引数を検査し、失敗すればプログラムを停止することができます。 assert は NDEBUG マクロでON/OFFを切り替えられます。assert はクラス不変条件には関知せず、 失敗したときに例外を投げることもしません。 メッセージを出力した後単にプログラムを終了させるだけです。 assert はマクロによるテキスト処理に依存しています。
標準C++での明示的なDbCサポートは、 assert 以上でも以下でもありません。
クラス不変条件
D のクラス不変条件の一例を考えてみます:class A { invariant() { ...契約... } this() { ... } // コンストラクタ ~this() { ... } // デストラクタ void foo() { ... } // publicメンバ関数 } class B : A { invariant() { ...契約... } ... }同じことをC++で書くには、 こうなります (Bob Bell の協力に感謝):
templateA::foo() には更に複雑になる要因があります。 関数からのどの通常の終了パスについても、 invariant() が呼び出されなくてはなりません。 これは、次のようなコードinline void check_invariant(T& iX) { #ifdef DBC iX.invariant(); #endif } // A.h: class A { public: #ifdef DBG virtual void invariant() { ...契約... } #endif void foo(); }; // A.cpp: void A::foo() { check_invariant(*this); ... check_invariant(*this); } // B.h: #include "A.h" class B : public A { public: #ifdef DBG virtual void invariant() { ...契約... A::invariant(); } #endif void bar(); }; // B.cpp: void B::barG() { check_invariant(*this); ... check_invariant(*this); }
int A::foo() { ... if (...) return bar(); return 3; }だとこうなることを意味します:
int A::foo() { int result; check_invariant(*this); ... if (...) { result = bar(); check_invariant(*this); return result; } check_invariant(*this); return 3; }あるいは、関数の終了場所が一カ所になるよう、返値を変数に記録して実現します。 RAII テクニックとして実現することもできるでしょう。
int A::foo() { #if DBC struct Sentry { Sentry(A& iA) : mA(iA) { check_invariants(iA); } ~Sentry() { check_invariants(mA); } A& mA; } sentry(*this); #endif ... if (...) return bar(); return 3; }check_invariant が何もしない関数であっても、 全てを最適化で除去することができないコンパイラもあるので#if DBCはまだ残っています。
事前条件・事後条件
次のDのコードをご覧下さい:void foo() in { ...事前条件... } out { ...事後条件... } body { ...実装... }これは Sentry 構造体を使って、C++ でうまく扱うことができます。
void foo() { struct Sentry { Sentry() { ...事前条件... } ~Sentry() { ...事後条件... } } sentry; ...実装... }事前条件や事後条件が単なるassertマクロからなっていれば、 全体を #ifdef で囲う必要もありません。 良いC++コンパイラなら、assert がOFFになっていれば全てのコードを最適化して除去してくれるでしょう。
しかし、foo() の中で配列がsortされていて、 事後条件で配列を走査して確実にソートされていることを検査する必要がある、 という状況を考えていましょう。こうなると、この仕掛けは #ifdef で囲い込まなければなりません:
void foo() { #ifdef DBC struct Sentry { Sentry() { ...事前条件... } ~Sentry() { ...事後条件... } } sentry; #endif ...実装... }("実際に使われるときのみインスタンス化される"というC++のtemplateの規則を使って、 条件検査関数を template にして assert から参照することで、 #ifdef を避けることも可能です。)
さあ、foo() に返値が加わって事後条件でチェックする場合を考えましょう。Dでは:
int foo() in { ...事前条件... } out (result) { ...事後条件... } body { ...実装... if (...) return bar(); return 3; }C++では:
int foo() { #ifdef DBC struct Sentry { int result; Sentry() { ...事前条件... } ~Sentry() { ...事後条件... } } sentry; #endif ...実装... if (...) { int i = bar(); #ifdef DBC sentry.result = i; #endif return i; } #ifdef DBC sentry.result = 3; #endif return 3; }foo() に引数が二つほど加わりました。D では:
int foo(int a, int b) in { ...事前条件... } out (result) { ...事後条件... } body { ...実装... if (...) return bar(); return 3; }C++では:
int foo(int a, int b) { #ifdef DBC struct Sentry { int a, b; int result; Sentry(int a, int b) { this->a = a; this->b = b; ...事前条件... } ~Sentry() { ...事後条件... } } sentry(a,b); #endif ...実装... if (...) { int i = bar(); #ifdef DBC sentry.result = i; #endif return i; } #ifdef DBC sentry.result = 3; #endif return 3; }
メンバ関数の事前条件・事後条件
Dの多相関数で 事前条件や事後条件を使う場合を考えてみます:class A { void foo() in { ...A事前条件... } out { ...A事後条件... } body { ...実装... } } class B : A { void foo() in { ...B事前条件... } out { ...B事後条件... } body { ...実装... } }B.foo() の呼び出しの際には、
- A事前条件 か B事前条件 のどちらかが満たされなくてはならない。
- A事後条件 と B事後条件 の両方が満たされなくてはならない。
class A { protected: #if DBC int foo_preconditions() { ...A事前条件... } void foo_postconditions() { ...A事後条件... } #else int foo_preconditions() { return 1; } void foo_postconditions() { } #endif void foo_internal() { ...実装... } public: virtual void foo() { foo_preconditions(); foo_internal(); foo_postconditions(); } }; class B : A { protected: #if DBC int foo_preconditions() { ...B事前条件... } void foo_postconditions() { ...B事後条件... } #else int foo_preconditions() { return 1; } void foo_postconditions() { } #endif void foo_internal() { ...実装... } public: virtual void foo() { assert(foo_preconditions() || A::foo_preconditions()); foo_internal(); A::foo_postconditions(); foo_postconditions(); } };少し面白い現象が発生しています。 結果のORを取る必要があるので、 事前条件のなかではもはや assert を使うことができません。 クラス不変条件を付け加えたり、 foo()関数に返値や引数を加えるのは、 読者への宿題としておきましょう。
結論
これらのC++のテクニックはかなりの程度までは動作します。しかし、assert を除いては標準化されていないため、プロジェクト毎に異なった方法が採られています。 それだけでなく、これらのテクニックでは特殊な規約に従ってややこしいコードを 書く必要がありますし、コードは相当ごちゃごちゃとなります。 多分これが、実際にDbCが使われているのをめったに見かけない原因でしょう。DbCサポートを言語として組み込むことで、D は DbC を正しく動かすための簡単な手段を提供しています。また言語標準とすることで、 どのプロジェクトでも使われるようになるでしょう。
参考文献
Object-Oriented Software Construction ( Bertrand Meyer, Prentice Hall ) の C.11章 で"契約プログラミング"の理論と根拠が導入されています。The C++ Programming Language Special Edition の 24.3.7.1~24.3.7.3節に、 C++における契約プログラミングについての議論があります。 ( Bjarne Stroustrup, Addison-Wesley )