C++ でテンプレートを使えば静的にポリモーフィックな Duck Typing というか,ある構文が有効であれば継承関係も何も無くても O.K. (専門用語で言えば,あるコンセプトのモデルを全て受け付ける)というコードを書くのは非常に簡単.ただし,一方で静的多相であること(コンパイル時決定であり,型の情報を引きずり倒さなければならない)が欠点となる状況も多い.動的多相が実現できない,コードの肥大化(code bloat)がある,など.
#後日注釈 (2005/11/17):実装をヘッダに書かないといけないという制約も大きい.
一方で C++ において,継承関係のないクラス間で動的多相を行う手法がないかというとそういうことはない.これを実現する技法は自分が知っている範囲で2つ.どちらも個人的に「型消去 (Type Erasure)」と呼んでる(で,実際にそう呼ばれることがある)技法.
1つは型を持った派生クラスを,型を持たない基底クラスで捉える手法.
#include <iostream> using namespace std; struct Duck{ void quack() const{ cout << "gaaa" << endl; } }; struct Foo{ void quack() const{ cout << "...." << endl; } }; struct Ducky{ struct DuckyHolderBase{ virtual ~DuckyHolderBase(){}; virtual void quack() const = 0; }; template<class T> struct DuckyHolder : public DuckyHolderBase{ DuckyHolder(T const &ducky) : ducky_(ducky) {} virtual void quack() const{ ducky_.quack(); } T ducky_; }; template<class T> Ducky(T const &ducky) : p_ducky_holder_(new DuckyHolder<T>(ducky)) {} ~Ducky() { delete p_ducky_holder_; } void quack() const{ p_ducky_holder_->quack(); } DuckyHolderBase *p_ducky_holder_; }; void func(Ducky ducky){ ducky.quack(); } int main(){ Duck duck; Foo foo; func(duck); func(foo); return 0; }
上のコードでは Ducky は値セマンティクスを持ったクラスだけれども,もちろん参照・ポインタセマンティクスを持たせるのもできる.
もう1つは手製の vtbl を initializer template で初期化する手法.
#include <iostream> using namespace std; struct Duck{ void quack() const{ cout << "gaaa" << endl; } void bark() const{ cout << "gagaga" << endl; } }; struct Foo{ void quack() const{ cout << "...." << endl; } void bark() const{ cout << "fufufu" << endl; } }; struct Ducky{ struct VTBL{ void (*quack)(void const *); void (*bark)(void const *); }; template<class T> struct VTBLInitializer{ static VTBL vtbl_; static void quack(void const *this_){ static_cast<T const *>(this_)->quack(); } static void bark(void const *this_){ static_cast<T const *>(this_)->bark(); } }; template<class T> Ducky(T &ducky) : this_(&ducky) , vptr_(&VTBLInitializer<T>::vtbl_) {} void quack() const{ vptr_->quack(this_); } void bark() const{ vptr_->bark(this_); } void *this_; VTBL *vptr_; }; // initializer template template<class T> Ducky::VTBL Ducky::VTBLInitializer<T>::vtbl_ = { &Ducky::VTBLInitializer<T>::quack , &Ducky::VTBLInitializer<T>::bark }; void func(Ducky ducky){ // ducky は参照として機能 ducky.quack(); ducky.bark(); } int main(){ Duck duck; Foo foo; func(duck); func(foo); return 0; }
こちらは参照・ポインタセマンティクスを持つ.値のセマンティクスは持てない.
どちらも動的多相で,かつ(少なくとも外側からみれば)完全に型安全.
前者の技法は Boost では普通に使われている.Boost.Any, Boost.Function などの実装が代表的.継承関係は全く無いけれど,要求されるインターフェースは持っているというクラス群を無節操かつ型安全にコンテナに放り込めるようにしているものもある(Boost.Iostreams の chain).
後者の技法は Boost.Interface (提案中)が使用している技法.こちらの技法はあまり使われているのを見たことがないけれど,個人的には発展性の高い技法ではないかと思う.
後者は仮想関数(ABI 互換性のネックになる)を用いていないので,ABI 互換性を維持するような工夫が可能(例えば保持するポインタを,想定するプラットフォーム全てで reinterpret_cast 可能な整数値型として保持するなど).実際この技法で,GCC (Cygwin, MinGW)でコンパイルした DLL 中で生成したクラスを MSVC でコンパイルした実行ファイルで使ったり,その逆をやったりする実験をやったこともある.
また,後者の技法を静的多相の文脈で使えば,コンパイラの最適化次第では inline 化も可能なはずなので,静的多相と動的多相両者の hyblid な文脈で有用なのではないかと思う.
両者とも型安全なものの,テンプレート特有の難解なコンパイルエラーを発生させる.ここらへんは Concept Check などで早期のコンパイル時ファイアウォールを形成することは可能.