読者です 読者をやめる 読者になる 読者になる

契約プログラミングでのオーバーライドの実現(D、C++、C#の実装比較)

 オブジェクト指向の定番の入門書OOSCでは、オブジェクト指向契約プログラミングの間に密接な関連付けを行っている。例えば継承によってメソッドをオーバーライドする場合では、Assertion Redeclaration ruleとして以下を示している。

  • 事前条件は、継承元と同等かより弱いもの(or-ed)に置き換えられる。
  • 事後条件は、継承元と同等かより強いもの(and-ed)に置き換えられる。

 今回は、このルールの実現度について、D、C++C#の3つぞれぞれで確認してみる。

結果まとめ

 Dは、OOSCのルール通りにオーバーライドを実装できる。記述も簡潔で、今回のテーマに限定すれば本家Eiffelのように契約プログラミングを行える。
 C++C++11の言語機能を使ったBoost.Contract(Boostに将来マージ予定)によって、OOCS通りの実装ができる。ただし違和感のある実装で、無理やり実現している感が否めない。
 C#Visual Studio拡張機能であるCode Contracts for .NET extensionを追加することで、契約プログラミングを簡潔に実装できるようになる。ただオーバーライド時の事前条件の評価方法が、OOSCのルールやDなどと異なる。

サンプルコード

 最低限の確認として、今回は以下を実装して動かす。

  • Bar::run()が、Foo::run()をオーバーライドする。
  • Bar::run()、Foo::run()はそれぞれ異なる事前条件、事後条件を持つ。
  • 「すべての事前条件・事後条件を満たす」「Bar::run()の事前条件のみ違反」「Bar::run()の事後条件のみ違反」「Foo::run()の事前条件のみ違反」「Foo::run()の事後条件のみ違反」の5パターンの動作を試す。

Dでの実装

 D言語契約プログラミングのサポートに注力しており、コードもすっきり書ける。コードは以下の通り。

import std.stdio;

class Foo
{
public:
    int run(int arg)
        in { //事前条件
            assert(arg % 3 == 0);
        }
        out (result) { //事後条件
            assert(result < 100);
        }
        body { //関数本体
            return arg * 3;
        }
}

class Bar : Foo 
{
public:
    override int run(int arg)
        in { //事前条件
            assert(arg % 2 == 0);
        }
        out (result) { //事後条件
            assert(result > 0);
        }
        body { //関数本体
            return arg * 2;
        }
}

void main()
{
    Foo foo = new Bar();
    writeln(foo.run(48));//success すべての事前条件・事後条件を満たす
    //writeln(foo.run(3));//success サブクラスの事前条件違反
    //writeln(foo.run(-6));//failure サブクラスの事後条件違反
    //writeln(foo.run(2));//success スーパークラスの事前条件違反
    //writeln(foo.run(51));//failure スーパークラスの事後条件違反
}

 Bar::run()の引数に応じた動作は、main()のコメントの「success」「failure」の通り。
 Bar::run()、Foo::run()の事前条件はORで結合されて評価されており、事後条件はANDで結合されて評価されているのがわかる。OOSCに忠実な動作となっている。

C++での実装

 C++では、契約プログラミングのライブラリとしてBoost.Contractが開発されている(正式なBoostへのマージは今後の予定。https://github.com/lcaminiti/boost-contract)。今回はそれを使用して実装した。
 なおC++では直接的にOOSCの契約プログラミングをサポートしていないため、記述がかなり煩雑になる。

#include <iostream>
#include <boost/contract.hpp>

class Foo 
{
protected:
    int result_;
public:
    virtual int run(int arg, boost::contract::virtual_* v = 0) {
        boost::contract::guard c = boost::contract::public_function(v, this)
            .precondition([&] { //事前条件
                BOOST_CONTRACT_ASSERT(arg % 3 == 0);
            })
            .postcondition([&] { //事後条件
                BOOST_CONTRACT_ASSERT(result_ < 100);
            })
        ;
        return (result_ = arg * 3); //本体
    }
};

class Bar 
    #define BASES public Foo
    : BASES
{
public:
    typedef BOOST_CONTRACT_BASE_TYPES(BASES) base_types;
    #undef BASES
    
    virtual int run(int arg, boost::contract::virtual_* v = 0) {
        boost::contract::guard c = boost::contract::public_function<override_run>(v, &Bar::run, this, arg)
            .precondition([&] { // 事前条件
                BOOST_CONTRACT_ASSERT(arg % 2 == 0);
            })
            .postcondition([&] { //事後条件
                BOOST_CONTRACT_ASSERT(result_ > 0);
            })
        ;
        return (result_ = arg * 2); //本体
    }
    BOOST_CONTRACT_OVERRIDE(run)
};

int main()
{
    Bar foo;
    std::cout << foo.run(48) << std::endl;//success すべての事前条件・事後条件満たす
    //std::cout << foo.run(3) << std::endl;//success サブクラスの事前条件違反
    //std::cout << foo.run(-6) << std::endl;//failure サブクラスの事後条件違反
    //std::cout << foo.run(2) << std::endl;//success スーパークラスの事前条件違反
    //std::cout << foo.run(51) << std::endl;//failure スーパークラスの事後条件違反

    return 0;
}

 Bar::run()への引数に応じた挙動は、D言語のものと同じになる。
 そのためC++でもオーバーライドでは契約プログラミングのルールを実現できている。

C#での実装

 C#も直接的にOOSCの契約プログラミングをサポートしていない。ただCode Contracts for .NET extensionを使用することによって、実装が容易になる(なおこのextensionはVC++でもVB.NETでも使える)。

using System;
using System.Diagnostics.Contracts;

class Foo
{
    public virtual int run(int arg)
    {
        Contract.Requires(arg % 3 == 0); // 事前条件
        Contract.Ensures(Contract.Result<int>() < 100); // 事後条件
        return arg * 3;
    }
}
class Bar : Foo
{
    public override int run(int arg)
    {
        Contract.Requires(arg % 2 == 0); // 事前条件
        Contract.Ensures(Contract.Result<int>() > 0); // 事後条件
        return arg * 2;
    }
}

class Program
{
    static void Main(string[] args)
    {
        int ret;
        ret = new Bar().run(48);//success すべての事前条件・事後条件を満たす
        //ret = new Bar().run(3);//failure サブクラスの事前条件違反
        //ret = new Bar().run(-6);//failure サブクラスの事後条件違反
        //ret = new Bar().run(2);//failure スーパークラスの事前条件違反
        //ret = new Bar().run(51);//failure スーパークラスの事後条件違反        
        Console.WriteLine(ret);
    }
}

 Code Contracts for .NETを使った場合、事後条件、事前条件ともに、AND結合されて評価されるようだ。「サブクラスの事前条件のみ違反」「スーパークラスの事前条件のみ違反」のときに、OOSCのルールと異なる判定結果を出している。