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 の協力に感謝):
template
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);
}
A::foo(). には更に複雑になる要因があります。
関数からのどの通常の終了パスについても、
invariant() が呼び出されなくてはなりません。
これは、次のようなコード
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 を正しく動かすための簡単な手段を提供しています。また言語標準とすることで、 どのプロジェクトでも使われるようになるでしょう。
参考文献
Bertrand Meyer の オブジェクト指向入門 第2版 原則・コンセプトの C.11章 で"契約プログラミング"の理論と根拠が導入されています。
Bjarne Stroustrup の
The C++ Programming Language Special Edition
の 24.3.7.1~24.3.7.3節に、C++における契約プログラミングについての議論があります。