第127回 Ruby vs Java ダックタイピングとインタフェースで見る多態性 - bingo_nakanishiの他言語出身者のためのPerl入門 を読んで、「静的な型付け言語もやればできる子なんです」と言うところを少しは見せる事ができればと、今さらなネタですが書き起こしてみました。ただし、ジェネリックに対応した言語である必要があります。
元の記事では Java でしたが、Java の環境がなかったので C++ で記述しています。Java も Java 5.0 以降(?)からジェネリックを取り入れたらしいので大きな違いはないと思います。
前置き
いま、このようにhumanがtouchすると、おのおのの動物が鳴くソースを書いてみる。duck(アヒル)はhuman(人間)に触られると「ガーガー」と鳴き、dog (犬)はhuman(人間)に触れると「ワンワン」と鳴くとする。
第127回 Ruby vs Java ダックタイピングとインタフェースで見る多態性 - bingo_nakanishiの他言語出身者のためのPerl入門
まず、前置きとして上記の記事の課題(ダックタイピング)を継承やインターフェースを用いずに実現するコードを記述してみます。
#include <iostream> class Duck { public: void say() const { std::cout << "ガーガー" << std::endl; } }; class Dog { public: void say() const { std::cout << "ワンワン" << std::endl; } }; class Human { public: template <class Animal> void touch(const Animal& target) { target.say(); } }; int main(int argc, char* argv[]) { Human human; Duck duck; Dog dog; human.touch(duck); human.touch(dog); return 0; }
実行結果
[clown@stinger example]$ g++ -Wall duck.cpp [clown@stinger example]$ ./a ガーガー ワンワン
template <class Animal> と言う記述以外は Ruby とほとんど同じ形で記述する事ができました。
Type Erasure
さて、この記事の本題です。上記のようにダックタイピングが単一の関数(メソッド)で閉じている場合は問題ないのですが、オブジェクトが渡されるタイミングと渡されたオブジェクトを実行するタイミングがずれる場合にはどうすれば良いのでしょうか?上記の例を流用するとすれば、例えば、以下のようにコンストラクタで触る対象となるオブジェクトを受け取る場合です。
class Duck; // 上のサンプルコードと同じ。 class Dog; // 〃 class Human { public: template <class Animal> Human(const Animal& target) : target_(target) {} void touch() { target_.say(); } private: ??? target_; // <- target_ は何型にすれば良い? }
Ruby などの動的な型付けの言語においては、このような場合でも問題なくダックタイピングを行うことができますが、静的な型付けの言語ではジェネリックの補助のみではこのような場合に対応することができません。
そこで、このような場合においても、静的な型付けの言語でもダックタイピングを行うことができるようなテクニックが編み出されました。このテクニックは Type Erasure と呼ばれています*1。
#include <iostream> #include <tr1/memory> class Duck; // 上のサンプルコードと同じ class Dog; // 〃 /* ------------------------------------------------------------------------- */ /* * BaseHolder * * BaseHolder がインターフェース的な役割を果たす。ただし、template * (generic) をうまく併用することによって、継承やインターフェース * を使用するための記述 (Java の場合だと extends hoge や implements hoge * と言う記述)をユーザから隠ぺいすることができる。 */ /* ------------------------------------------------------------------------- */ class BaseHolder { public: BaseHolder() {} virtual ~BaseHolder() throw() {} virtual void run() = 0; }; /* ------------------------------------------------------------------------- */ // Human /* ------------------------------------------------------------------------- */ class Human { public: template <class Animal> Human(Animal target) : p_(new AnimalHolder<Animal>(target)) {} void touch() { p_->run(); } private: typedef std::tr1::shared_ptr<BaseHolder> holder_ptr; /* --------------------------------------------------------------------- */ /* * AnimalHolder * * 実際には、この AnimalHolder が BaseHolder を継承することによって * 多態性を実現している。ただし、この AnimalHolder が Human クラス * のコンストラクタでユーザから指定されたオブジェクトの型に応じて * 変化することによって、継承関係(やインターフェース)をユーザから * 隠ぺいすることができる。 */ /* --------------------------------------------------------------------- */ template <class T> class AnimalHolder : public BaseHolder { public: AnimalHolder(T target) : target_(target) {} virtual ~AnimalHolder() throw() {} virtual void run() { target_.say(); } private: T target_; }; holder_ptr p_; }; /* ------------------------------------------------------------------------- */ // main /* ------------------------------------------------------------------------- */ int main(int argc, char* argv[]) { Duck duck; Dog dog; Human h1(duck); h1.touch(); Human h2(dog); h2.touch(); return 0; }
BaseHolder の存在がポイントです。第127回 Ruby vs Java ダックタイピングとインタフェースで見る多態性 - bingo_nakanishiの他言語出身者のためのPerl入門 の Java における解法にもあったように、(ジェネリックの助けを借りずに)静的な型付けの言語においてダックタイピング(っぽいこと)を行おうとする場合には、基底クラスをあらかじめ定義しておき、その基底クラスから派生させることによって実現します。
Type Erasure においても、基本的にはこの方法を用いています。上記の例では、BaseHolder と言う基底クラスを最初に作成しておき、実際に実行するオブジェクト (template <class T> class AnimalHolder) はこの BaseHolder を継承することによって多態性を実現します。
ただし、この Type Erasure と呼ばれるテクニックの上手いところは BaseHolder を継承した AnimalHolder をテンプレート・クラス(ジェネリック)にしておくことで、ユーザから渡されるオブジェクトの型によって生成するオブジェクトを変えられるようにしている点です。この仕掛けのおかげで、ユーザに class Duck : public Animal のように継承関係を明示させることを強いることなく、見かけ上は Ruby などの動的型付け言語と同様のダックタイピングを実現することができます。
Type Erasure はジェネリックと継承を併用した、多態勢を実現するための強力な手段の一つです。私が Ruby などのような動的な型付けの言語と比較しても特に大きな差(不便)を感じることなく C++ でコーディングする事ができる理由の一つに、この Type Erasure の存在があるのだろうと思います。
*1:Type Erasure と言う名前は C++ テンプレートテクニック(επιστημη、高橋晶著)を読むまで知りませんでした。