2012年10月21日日曜日

猿でも分かった~型消去技法とは

ひさびさにC++の話。

C++の技法に型消去(Type Erasure)というものがありますね。なんかかっこいい名前だし、いろんなすごいライブラリで使われてるっぽい話はよく聞くので、「Type Erasure? なにそれ? やだ、こわーい」となる人もいるでしょう。(ぼくもそう)
でも型消去はトモダチ、怖くない!

唐突ですが、継承関係はないけどとにかく"f"という関数を持ったクラス群があったとしましょう。
struct A { void f() { cout << "A!"; } }; struct B { void f() { cout << "B!"; } };
役にたつかどうかは分からんけど、こういったものを保持しておいて後からfを呼び出すことができるクラスholderが作れるか考えてみましょう。
これは簡単で、テンプレートを使って以下のように書けますね。
template<typename T> struct holder { holder(const T& obj) : m_obj(obj) {} void f() { m_obj.f(); } T m_obj; }; holder<A> a(A()); holder<B> b(B()); a.f(); b.f();
さてここでholderはテンプレートクラスですが、テンプレートでないクラスにすることはできるでしょうか? もしこれができれば、以下のようにとにかくfという関数をもったあらゆるオブジェクトをobjsという配列に溜め込んで、その後適切にfを呼び出すようなことができます。
vector<holder> objs; ... for (size_t i = 0; i < objs.size(); ++i) { objs[i].f(); }
驚くべきことに、これが実に単純な方法で実現できます。仮想関数とテンプレートを組み合わせて使って型に関する情報を派生クラス側に追い出すだけです。
struct holder { virtual void f() = 0; }; template<typename T> struct holder_sub : public holder { holder_sub(const T& obj) : m_obj(obj) {} virtual void f() { m_obj.f(); } T m_obj; };
使ってみましょう。
vector<shared_ptr<holder> > objs; objs.push_back(new holder_sub<A>(A()); objs.push_back(new holder_sub<B>(B()); for (size_t i = 0; i < objs.size(); ++i) { objs[i]->f(); }
ついでに、使うときに派生クラスを見せるのはかっこ悪いので以下のようなラッパーをかませましょう。
struct anyf { template<typename T> anyf(const T& a) { m_p.reset(new holder_sub<T>(a)); } void f() { m_p->f(); } shared_ptr<holder> m_p; };
するとこんな風に使えます。
vector<anyf> objs; objs.push_back(A()); objs.push_back(B()); for (size_t i = 0; i < objs.size(); ++i) { objs[i].f(); }
anyfにはfをもった要素ならなんでも突っ込めるし、そうでなければちゃんとコンパイルエラーになってくれます。
これが型消去技法です。きわめて単純なコードなのに、魔法みたいな挙動が実現できてますね!

これまでのサンプルの関数fは非常につまらない関数でしたが、元の型にキャストするような関数に適用するとどうでしょうか?
そう、boostの超便利クラスanyみたいなものが出来そうです。anyも基本的にはこの単純な型消去技法を使って実装されているようです。