C++でVisitorパターンについて, Modern C++ Designに目を通しながら自分なりの理解をまとめてみた.
ポイントはVisitorパターンの存在意義と, C++でのその実現方式を分けて考えることか.
Visitorパターンの目的
まず, 大前提としてC++のようなオブジェクト指向プログラミングでインタフェースクラスを用いた場合のPros, Consとして
- Pros.「新しいコンポーネント」をインタフェースクラスの子クラスとして追加することは簡単.
- Cons. インタフェースクラスに「新しい機能」 = 仮想関数を追加するのは簡単ではない. これは、継承先の子クラス全体に新しい仮想関数を書き加えなければならないため.
という事実がある.
さて、現実にはすでに存在するインタフェースクラスおよびこれを継承する一連のクラス群に、クラスごとに処理の詳細が異なる新しい機能を同一のインタフェースで提供したくなることは良くある.
このようなケースにおいて、愚直に既存のインタフェースクラスに仮想関数を追加するのは、これを継承している既存コンポーネント全体の書き換えを要求するので避けたい.
では、どうすれば良いのか?
Visitor Patternは、このようなプログラミング上の問題に対するスマートな解決策の一つである、と位置づけることができる.
Visitor パターンの実装方法
C++でVisitorパターンを実装するさいに良く用いられるのがDynamic Dispatchと呼ばれる手法である. 詳細は下のサンプルコードを参照.
これが少しトリッキーで Visitorパターンの少し分かりにくい目的と相まって, その理解を妨げるものになっていると思われる.
ミニマルな例を挙げる.
#include <iostream>
using namespace std;
class Derived1;
class Derived2;
class Visitor;
class Base
{
public:
virtual void accept(Visitor &iVisitor) = 0;
};
class Visitor
{
public:
virtual void visit(Derived1 & param1) = 0;
virtual void visit(Derived2 & param1) = 0;
};
class Derived1: public Base
{
public:
virtual void accept(Visitor &iVisitor) { iVisitor.visit(*this); }
};
class Derived2: public Base
{
public:
virtual void accept(Visitor &iVisitor) { iVisitor.visit(*this); }
};
class Printer1: public Visitor
{
public:
void visit(Derived1 & param1) { cout << "Deeeeeeerived1\n"; }
void visit(Derived2 & param1) { cout << "Deeeeeeerived2\n"; }
};
class Printer2: public Visitor
{
public:
void visit(Derived1 & param1) { cout << "Derived1111111\n"; }
void visit(Derived2 & param1) { cout << "Derived2222222\n"; }
};
int main() {
Derived1 d1;
Derived2 d2;
Printer1 p1;
Printer2 p2;
d1.accept(p1); // > Deeeeeeerived1
d2.accept(p1); // > Deeeeeeerived2
d1.accept(p2); // > Derived1111111
d2.accept(p2); // > Derived2222222
}
まず、Visitorとこれを受け入れるBaseクラスを定義している.
その後、Baseクラスを継承するDerived1, Derived2クラスで、上記のように自分自身へのポインタをvisitorに渡すだけのaccept関数を仕込んでおく.
あとは、Visitorクラスの派生クラスを定義することで、いくらでも新しい機能をDerive1, Derived2 に追加していける. 例では、単純に出力される文字列を変更しているだけだが、引数で渡ってくるクラスに依存した処理をVisitor内で書くことが可能になっている点が重要な点.
さらにもう一歩
Visitor Patternの実装に用いられるDynamic Dispatchを用いると、既存のコンポーネント群を書き換えることなくに新しい関数を定義することができる(したかのように魅せる)だけでなく、例えば
- 同じ名前の関数だが、引数に渡される変数の型の組み合わせごとに処理の内容を変更したい
といった要求もスマートに解決することができる.
引数が二つの場合にこれを実装したものが、下記のサンプルコードになる。
#include <iostream>
using namespace std;
class Derived1;
class Derived2;
class Visitor;
class Base
{
public:
virtual void Accept(Visitor &iVisitor, Base& param1) = 0;
virtual void Accept(Visitor &iVisitor, Derived1& param2) = 0;
virtual void Accept(Visitor &iVisitor, Derived2& param2) = 0;
};
class Visitor
{
public:
virtual void Visit(Derived1 & param1, Derived1 ¶m2) { cout << "11\n"; }
virtual void Visit(Derived1 & param1, Derived2 ¶m2) { cout << "12\n"; }
virtual void Visit(Derived2 & param1, Derived1 ¶m2) { cout << "21\n"; }
virtual void Visit(Derived2 & param1, Derived2 ¶m2) { cout << "22\n"; }
};
class Derived1: public Base
{
public:
virtual void Accept(Visitor &iVisitor, Base& param1)
{ param1.Accept(iVisitor, *this); }
virtual void Accept(Visitor &iVisitor, Derived1& param2)
{ iVisitor.Visit(*this, param2); }
virtual void Accept(Visitor &iVisitor, Derived2& param2)
{ iVisitor.Visit(*this, param2); }
};
class Derived2: public Base
{
public:
virtual void Accept(Visitor &iVisitor, Base& param1)
{ param1.Accept(iVisitor, *this); }
virtual void Accept(Visitor &iVisitor, Derived1& param2)
{ iVisitor.Visit(*this, param2); }
virtual void Accept(Visitor &iVisitor, Derived2& param2)
{ iVisitor.Visit(*this, param2); }
};
void Visit(Visitor& visitor, Base& param1, Base& param2)
{
param2.Accept(visitor, param1);
}
int main() {
Derived1 d1;
Derived2 d2;
Visitor v;
Visit(v, d1, d1);
Visit(v, d1, d2);
Visit(v, d2, d1);
Visit(v, d2, d2);
}
余談
Visitorパターンという名前はデザインパターンの動作を示してはいても, その目的や存在意義を直接表していないように思う. コンポーネント追加とインタフェースの追加を交換可能にする点が本質なわけで、これを表す良い名前はないものか.