英語版
このページの英語版を見る

D's Contract Programming vs C++'s

Dの「契約プログラミング」は C++がすでにサポートしていないものを追加するものではないと、多くの人々が私に書き送ってきた。 彼らはさらに、C++で「契約」を行うためのテクニックを挙げて、その主張を裏付けようとしている。

契約プログラミングとは何か、Dではどのように行うのか、 そして、C++のさまざまな契約技術がそれぞれ何ができるのかを比較検討することは意味がある。

C++では、 C++に拡張機能を追加して 契約をサポートしているが、それらは標準C++の一部ではなく、また他のC++コンパイラでもサポートされていないため、ここでは取り上げない。

Digital Mars C++は C++に拡張機能を追加して 契約をサポートしているが、それらは標準C++の一部ではなく、また他のC++コンパイラでもサポートされていないため、ここでは取り上げない。

D言語における契約プログラミング

この点については、D 契約プログラミング 文書でより詳しく説明されている。 要約すると、Dにおける契約プログラミングには以下の特徴がある。
  1. assertが基本的な契約である。
  2. assert契約が失敗すると、例外がスローされる。 このような例外は捕捉して処理することも、 プログラムを終了させることもできる。
  3. クラスには、クラス不変値を設定することができ、 これは各パブリッククラスメンバ関数のエントリとエグジット、 各コンストラクタのエグジット、およびデストラクタのエントリ時に チェックされる。
  4. オブジェクト参照の契約は、 そのオブジェクトのクラス不変性をチェックする。
  5. クラス不変値は継承される。つまり、派生 クラス不変値は暗黙のうちにベースクラス不変値を呼び出すことになる。
  6. 関数には事前条件事後条件を設定できる。
  7. クラス継承階層におけるメンバ関数では、 派生クラスの関数の前提条件は、 オーバーライドするすべての関数の前提条件とORで結合される。 事後条件はANDで結合される。
  8. コンパイラスイッチをスローすることで、契約コードを有効にしたり、 またはコンパイル済みコードから取り除くことができる。
  9. コードは、契約チェックを有効にしても無効にしても、意味的には同じように動作する。

C++での契約プログラミング

assert マクロ

C++には、引数をテストし 、失敗した場合はプログラムを中断する基本的なxml-ph-0000@deepl.internalマクロが存在する。xml-ph-0001@deepl.internal

C++には、引数をテストし 、失敗した場合はプログラムを中止する基本的なassert マクロがある。assertNDEBUG マクロでオン・オフできる。

assert クラス不変については何も知らないため、 失敗しても例外はスローされない。単に メッセージを表示した後にプログラムを終了するだけである。 は、 動作するにはマクロテキストプリプロセッサに依存する。assert

assert 標準C++における契約の明示的なサポートは、 ここから始まり、ここでおしまいだ。

クラスの不変量

Dにおけるクラス不変を考えてみよう。
class A
{
    invariant() { ...contracts... }

    this() { ... }      // コンストラクタ
    ~this() { ... }     // デストラクタ

    void foo() { ... }  // 公開メンバ関数
}

class B : A
{
    invariant() { ...contracts... }
    ...
}
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() { ...contracts... }
#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()
        {   ...contracts...
           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;
}
#if DBCは、一部のコンパイラが check_invariantsが何もコンパイルされない場合に全体を最適化しない可能性があるため、依然として存在します。

前提条件と事後条件

Dでは、以下の点を考慮する。
void foo()
in { ...preconditions... }
out { ...postconditions... }
body
{
    ...implementation...
}
これは、ネストされた Sentry 構造体を使用することで、C++ ではうまく処理できる。
void foo()
{
    struct Sentry
    {
        Sentry() { ...preconditions... }
        ~Sentry() { ...postconditions... }
    } sentry;
    ...implementation...
}

前提条件と事後条件がassert マクロのみで構成されている場合、全体を#ifdef ペアでラップする必要はない。なぜなら、優れたC++コンパイラはassertがオフになっている場合、全体を最適化して取り除くからだ。

しかし、foo() が配列をソートし、その事後条件として 配列を順にたどり、実際にソートされていることを確認する必要があるとしよう。 この場合、シェバングは#ifdef で囲む必要がある。

void foo()
{
#ifdef DBC
    struct Sentry
    {
        Sentry() { ...preconditions... }
        ~Sentry() { ...postconditions... }
    } sentry;
#endif
    ...implementation...
}

(C++のルールである「テンプレートは使用されたときにのみインスタンス化される」というルールを利用すれば、#ifdef を回避できる。assert で参照されるテンプレート化された関数に条件を入れることで、

foo() に返り値を追加し、事後条件で確認する必要がある。 Dでは:

C++では: xml-ph-0000@deepl.internal xml-ph-0001@deepl.internal にいくつかのパラメータを追加する。Dでは:
int foo()
in { ...preconditions... }
out (result) { ...postconditions... }
body
{
    ...implementation...
    if (...)
        return bar();
    return 3;
}
C++では:
int foo()
{
#ifdef DBC
    struct Sentry
    {
        int result;
        Sentry() { ...preconditions... }
        ~Sentry() { ...postconditions... }
    } sentry;
#endif
    ...implementation...
    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 { ...preconditions... }
out (result) { ...postconditions... }
body
{
    ...implementation...
    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;
            ...preconditions...
        }
        ~Sentry() { ...postconditions... }
    } sentry(a, b);
#endif
    ...implementation...
    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 { ...Apreconditions... }
    out { ...Apostconditions... }
    body
    {
        ...implementation...
    }
}

class B : A
{
    void foo()
    in { ...Bpreconditions... }
    out { ...Bpostconditions... }
    body
    {
        ...implementation...
    }
}
B.foo() への呼び出しのセマンティクスは以下の通りである。 これをC++で動作させてみよう。
class A
{
protected:
    #if DBC
    int foo_preconditions() { ...Apreconditions... }
    void foo_postconditions() { ...Apostconditions... }
    #else
    int foo_preconditions() { return 1; }
    void foo_postconditions() { }
    #endif

    void foo_internal()
    {
        ...implementation...
    }

public:
    virtual void foo()
    {
        foo_preconditions();
        foo_internal();
        foo_postconditions();
    }
};

class B : A
{
protected:
   #if DBC
    int foo_preconditions() { ...Bpreconditions... }
    void foo_postconditions() { ...Bpostconditions... }
    #else
    int foo_preconditions() { return 1; }
    void foo_postconditions() { }
    #endif

    void foo_internal()
    {
        ...implementation...
    }

public:
    virtual void foo()
    {
        assert(foo_preconditions() || A::foo_preconditions());
        foo_internal();
        A::foo_postconditions();
        foo_postconditions();
    }
};
ここで興味深いことが起こっている。前提条件は もはやassert を使用して行うことはできない。なぜなら、結果は ORで結合する必要があるからだ。 クラス不変、foo() の関数戻り値、 および foo() のパラメータを 追加する練習問題は読者に任せる。

結論

これらのC++テクニックはある程度までは有効である。しかし、assert を除いては、標準化されていないため、プロジェクトごとに異なる。 さらに、 特定の慣例に厳密に従う必要があり、コードが非常に煩雑になる。 おそらく、それが実務ではほとんど見られない理由だろう。

D言語に契約のサポートを追加することで、D言語は 契約を簡単に使用し、正しく実装する方法を提供する。言語に組み込むことで、 プロジェクト間で統一された方法で使用できるようになる。

参考文献

第C.11章では、 契約プログラミングの理論と根拠を オブジェクト指向ソフトウェア構築

第C.11章では、 「契約プログラミング」の理論と根拠を 「オブジェクト指向ソフトウェア構築」 (日本語版は上巻下巻に分かれる)で
バートランド・メイヤー著、Prentice Hall

第24.3.7.1章から第24.3.7.3章では、C++における契約プログラミングについて 『The C++ Programming Language Special Edition』
Bjarne Stroustrup、Addison-Wesley