空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

Double Dispatch (Visitor pattern) に関する考察 (3)

id:xoinu:20070629,id:xoinu:20070630:1183260530 の続き。で,ようやく私なりに考えて,double dispatch(Visitor パターン)について思うことを2つ書いてみたい。

Visitor パターンの幻想?

Visitor パターンの本質は double dispatch にある。で,double dispatch の本質は多態を使って条件文(if,switch)を無くすことだ。Web を見ていたら,Visitor パターンの役割はデータ構造と処理の分離であるとあったけど,それってデータのオブジェクトをポインタで渡して,渡した先で何か処理すしてるだけぢゃ…と言いたくなる。それは,たまたまそうなっているだけだと思う。

double dispatch のメリット

私がここまで興味を持って考えたのは,保守性の高いライブラリを設計するにあたって,double dispatch というのは役に立つんじゃないかと考えているからだ。けっきょく,double dispatch によって何が変わったのか?それは Nattou::EatenBy に集約されている。

void Nattou::EatenBy(Animal *pA)
{
    class NattouDispatcher : public Dispatcher
    {
        Nattou *_this_;
    public:
        explicit NattouDispatcher(Nattou *p) : _this_(p){}
        virtual void Dispatch(Cat    *pC) { _this_->EatenByCat(pC); }
        virtual void Dispatch(Dog    *pD) { _this_->EatenByDog(pD); }
        virtual void Dispatch(Monkey *pM) { _this_->EatenByMon(pM); }
        virtual void Dispatch(Snake  *pS) { _this_->EatenBySnk(pS); }
    };

    NattouDispatcher dispatcher(this);
    pA->Dispatch(&dispatcher);
}

結局これは,やってることは

void Nattou::EatenBy(Animal *pA)
{
    do
    {
        {
            Cat *pC = dynamic_cast<Cat*>(pA);
            if (pC)
            {
                EatenByCat(pC);
                break;
            }
        }
    
        {
            Dog *pD = dynamic_cast<Dog*>(pA);
            if (pD) 
            {
                EatenByDog(pD);
                break;
            }
        }
             :
             :
    }
    while (0);
}

と,全く一緒。このやり方では,dynamic_cast が遅いというなら,Animal に enum 型のタイプを返すメソッドを用意しておいて

void Nattou::EatenBy(Animal *pA)
{
    switch (pA->Type())
    {
    case CAT:
        EatenByCat(static_cast<Cat*>(pA));
        break;
    case DOG:
        EatenByDog(static_cast<Dog*>(pA));
        break;
    case MONKEY:
        EatenByMonkey(static_cast<Monkey*>(pA));
        break;
    case SNAKE:
        EatenBySnake(static_cast<Snake*>(pA));
        break;
    default:
        assert(!"Never reach here.");
        break;
    }
}

とすれば,恐らく double dispatch(仮想関数テーブル)よりも高速な実装になる。C++で douoble dispatch をやる場合には,一旦別のクラスのコードに処理が移るぶんだけ,複雑になっている。多くの場合は Type() と switch で十分だろう。
しかし,switch のやり方だと,基底クラスに人為的な enum と,タイプメンバを持たせるという無駄が生じてしまうのと,C++ の抽象クラスの多態性を活用せずに実行時のC++の型情報を無視して static_cast してしまっている。さらに,条件文を書き下す方式だと,どうしても人為的なミス,修正し忘れ,モレが生じてしまうことを免れない。

プロジェクトを引き継ぐ際に後任者に「Animal の種類を追加するときは,Type() で分岐している switch 文と if 分の箇所を,全部修正してね」なんて教えるなんて事態(よくある)は,プログラマとしては敗北感を感じずにはいられない。

その点,double dispatch の場合は,おおざっぱな話,新しい動物を追加する際には Dispatcher に純粋仮想関数を追加してビルドしてエラーが全部無くなるように実装するだけで,動物が追加できる。つまり条件文のシンタックスを関数という形に変換することで,コンパイル時に switch 文の実装漏れを検出できるようになる。