Entry

プログラミングメモ - CRTP + Strategy/Template パターンとか

2010年03月21日

あまり大した話でもないんですけど,メモ。

いくつかの似たようなオブジェクトをまとめたくなったとき,統一したインターフェイス(基底クラス)を持つ派生クラスを書くことがよくあります。というか,継承関係って,もっぱらそのために使われている。

で,例えば,次のようなことがしたいとします。

#include <iostream>

int main(int argc, char* argv[]) {
  A a;  // A 型のオブジェクト a
  B b;  // B 型のオブジェクト b

  // clone() を使う
  Object* oa = a.clone();
  Object* ob = b.clone();
  // type() を使う
  std::cout << oa->type() << std::endl;
  std::cout << ob->type() << std::endl;

  delete oa;
  delete ob;

  return 0;
}

ここで,Object 型というのは,A と B に共通する基底クラスだと考えてください。最初の clone メソッドは,自分自身の複製を作って返すメソッドです。これは,クラスA,クラスB それぞれのクラスで定義することができます。また,type メソッドは,自分自身について type_info::name() の値を返すクラス。こっちはというと,Object 型のポインタから利用していることから,少なくとも Object クラスに宣言が必要です。

さて,ここでクラス A とクラス B をクラス Object としてまとめておくと,何が嬉しいのでしょう。例えばこうしておくと,異なる型を,次のようなひとつのコンテナにまとめることができたりします。ま,これは,以前も書いた通り。そして,Object* 型のままディープコピーするときは,各オブジェクトの複製を作る必要があります。このときに使うのが Object::clone メソッドです。

std::vector<Object*> objects;

で,こゆもんは普通,Strategy/Template パターンを使って実装すると,すんなり書けたりします(Strategy とも Template とも言えそうなので,ふたつ並べて書いています)。つまり,Object クラスでは,インターフェイス(純粋仮想関数)として clone メソッドを宣言するだけにしておいて,各派生クラスがそれぞれの仮想関数内で,具体的な処理を実装するわけです。A::clone メソッドだったら,こんな風に書ける。type メソッドも同様です。

Object* A::clone() throw () {
  return new A(*this);
}

一方で,困ったことに,Strategy/Template パターンをそのまま使うと,似たような実装がとても増えてしまいます。上の clone メソッドを見ても分かる通り,B::clone メソッドを作ろうとすると,A::clone とほとんど変わらない,次のような関数を定義しなくちゃいけなくなります。

Object* B::clone() throw () {
  return new B(*this);
}

あほらしいですね。

いや,単に愚直なだけなら問題ないんです。しかし,派生クラスの数が100とか200とかになっても,こゆ機械的なコードを入れるのは,ものすごく無駄です。また,上のような単純なコードではバグが入る余地も少ないけれど,派生クラスが増えきった後にバグが見つかった,なんていったら大変です。コピペし直さなきゃいけない。

ということで,Strategy/Template パターンを使うにしても,ほとんど同じようなコードになるときは,こいつもまとめたいところです。そこで出てくるのが CRTP(Curiously Reccursive Template Pattern; 奇妙に再帰したテンプレートパターン)。CRTP については,詳しく説明されているところがいくつかあるので,なじみのない方はぐぐってみて欲しいんですけれど,かいつまんで言うと,

template<typename Derived> class Base { ... };
class Derived : public Base<Derived> { ... };

のように,基底クラスが派生クラスをテンプレートパラメタとして持っているようなパターンです。こうしておくと,派生クラスの実装を基底クラスで行うことができるので,便利便利というわけ。

で,こいつをさっきの継承関係の間に挟みこんでみたらどうなるでしょうか。Strategy/Template パターンと組み合わせたものを書いてみます。

#include <typeinfo>

////////////////////////////////////////////////////////////
// 最上位のクラス(インターフェイス)
class Object {
protected:
  Object() throw ();
public:
  virtual ~Object() throw ();
private: // no implement
  Object(const Object& other) throw ();
  Object& operator=(const Object& rhs) throw ();
public:  // インターフェイスをいくつか宣言
  virtual Object* clone() throw () = 0;
  virtual const char* type() throw () = 0;
};

Object::Object() throw () {
}

Object::~Object() throw () {
}

////////////////////////////////////////////////////////////
// インターフェイスの実装クラス
template<typename T>
class Base : public Object {
public:
  Base() throw ();
  Base(const Base& other) throw ();
  virtual ~Base() throw ();
public:
  Base& operator=(const Base& rhs) throw ();
public:
  virtual Object* clone() throw ();
  virtual const char* type() throw ();
};

template<typename T>
Base<T>::Base() throw () {
}

template<typename T>
Base<T>::Base(const Base<T>& other) throw () {
}

template<typename T>
Base<T>::~Base() throw () {
}

template<typename T>
Base<T>& Base<T>::operator=(const Base<T>& rhs) throw () {
  return *this;
}

// Object::clone() の実装(派生クラスで共通するメソッド)
template<typename T>
Object* Base<T>::clone() throw () {
  return new T(*static_cast<T*>(this));
}

// Object::type() の実装(派生クラスで共通するメソッド)
template<typename T>
const char* Base<T>::type() throw () {
  return typeid(T).name();
}

////////////////////////////////////////////////////////////
// 派生クラス A
class A : public Base<A> {
public:
  A() throw ();
  A(const A& other) throw ();
  virtual ~A() throw ();
public:
  A& operator=(const A& rhs) throw ();
public:
  // clone(), type() メソッドの宣言・定義は不要
};

A::A() throw () {
}

A::~A() throw () {
}

A::A(const A& other) throw () {
}

A& A::operator=(const A& rhs) throw () {
  return *this;
}

////////////////////////////////////////////////////////////
// 派生クラス B
class B : public Base<B> {
public:
  B() throw ();
  B(const B& other) throw ();
  virtual ~B() throw ();
public:
  B& operator=(const B& rhs) throw ();
public:
  // clone(), type() メソッドの宣言・定義は不要
};

B::B() throw () {
}

B::~B() throw () {
}

B::B(const B& other) throw () {
}

B& B::operator=(const B& rhs) throw () {
  return *this;
}

////////////////////////////////////////////////////////////
// クライアント
#include <iostream>

int main(int argc, char* argv[]) {
  A a;  // A 型のオブジェクト a
  B b;  // B 型のオブジェクト b

  // clone() を使う
  Object* oa = a.clone();
  Object* ob = b.clone();
  // type() を使う
  std::cout << oa->type() << std::endl;
  std::cout << ob->type() << std::endl;

  delete oa;
  delete ob;

  return 0;
}

注目すべきところは,A::clone メソッドと B::clone メソッドが宣言・実装されていないこと。共通しているから書かなくていいんですね。で,共通化した処理はどこにあるかというと,Base<T>::clone メソッドにまとまっています。type メソッドについても同様です。なんで,基底クラスが派生クラスのメソッドを実装できるかというと,各派生クラスの宣言時に,

class A : public Base<A> {
  // ...
}

と,基底クラスのテンプレートパラメタに,自分自身(A)を与えているからだったのでした。こうしておけば,Base<T> クラスを継承するだけで,自動的に自分自身の複製を作るメソッドを作ることができます。

一方,共通化された関数が列挙されているなら,CRTP だけで全部をまとめることができるんでねいの?(Object はいらないんでねいの?)という疑問も沸いてきます。しかし,これは多分無理。なぜかというと,Object* 型で受けないとしたら Base<T>* 型でポインタを受けることになるわけですけれど,ここで Base<T>* 型の型名に,派生クラスの名前が必要だからです。

  Base<A>* oa = a.clone();
  Base<B>* ob = b.clone();

同じ型で受けられないから,冒頭のウマミであった,コンテナに格納することもできません。こゆの,できるといいんですけどね。

ま,ただそれだけ。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN