C++
C++で動的ポリモーフィズムをしてみる
オブジェクト指向型言語の普及に一役買った言語は多数ありそうだが、C++もそのひとつだと思う。
当時オブジェクト指向型言語の3本柱と言われたものがあり、それが「カプセル化・継承・ポリモーフィズム」だ。
C++はこのすべてが行えるコンパイル型言語として生まれ、それは今も変わっていない。
「ポリモーフィズム」というのは、この頃の考え方で言えば「基底クラスが派生クラスへ振る舞いを変えること」だ。
要するに、親インスタンスが持っているメンバ関数が、ある時を境に子インスタンスが持っている同名のメンバ関数に置き換わる、ということだ。
時代が進み、オブジェクト指向が動的型付け言語にも取り入れられるようになると、動的型付け言語の利点を生かしたポリモーフィズムが生まれてくる。
その代表的なものが「ダックタイピング」だと思う。
すべてのインスタンスに同名・同シグネチャの関数(メソッド)があれば、それらに継承関係が無くても実際の使用に差し支えないではないか、という考えだ。
動的型付け言語ならではの発想と動作原理であり、静的型付け言語には苦手なポリモーフィズムだ。(良い悪いは別としても、この手法をあまり好まない人は一定数いるように思う。私自身、仕事で書くコードに好んでダックタイピングを使うことは無いが、動的型付け言語の場合意図せずダックタイピングになってしまうことはある)
その後C++には関数テンプレートが採用され、GoではStructuralTypingのような仕組みが採用され、ダックタイピングのように記述できる静的型付け言語が増えてくる。
静的型付け言語のこのような手法を「静的ポリモーフィズム」と呼ぶようになり、元々のポリモーフィズムはレトロニムになって「動的ポリモーフィズム」と呼ばれるようになった。
動的ポリモーフィズムを支えるものは「クラスの継承」と「関数のオーバーライド」そして「アップキャスト」だ。
C++では親クラスに「仮想関数」を書いた上で、その子クラスに同じ名前のメンバ関数を書くと、親クラスと子クラスに同じ名前で中身が違うメンバ関数ができる。
この状態を「子クラスの○○関数は親クラスの○○関数をオーバーライドしている」という。
まず親クラスのインスタンスを生成して、変数に格納したとする。
この変数は親クラスの型になる。
このときにオーバーライドした名前の関数を呼ぶと、当然親クラス側に書いた動作になる。
この変数に、子クラスのインスタンスを代入(上書き)しようとした場合、動的型付け言語ならばなんということもないが、静的型付け言語はそうはいかない。
親子で型が違うからだ。
型が違う変数を代入するためにはキャストをすれば良い。
つまり子クラスのインスタンスを、親クラスの型にキャストすれば代入(上書き)できるわけだ。
これがアップキャストだ。(ちなみにC++は暗黙にアップキャストしてくれる)
子クラスのインスタンスを代入した後に同じメンバ関数を呼ぶと、今度は子クラス側に書いた動作に変わる。
「インスタンスの型は親クラスのままなのに、呼び出した関数の動作が子クラスのものになる」、これが動的ポリモーフィズムだ。
これの何が便利なのかというと、例えばゲームを作る時に「一定ダメージをくらうと攻撃がパワーアップする敵キャラ」を作りたい時などに役に立つ。
はじめは親クラスのインスタンスを敵キャラ変数に格納して、攻撃ルーチンの入った関数を呼び出す。
残り体力が半分以下になったら子クラスのインスタンスを生成し、アップキャストして敵キャラ変数に上書きするだけでいい。
その後は同じように攻撃ルーチンの入ったメンバ関数を呼び出せば、パワーアップした攻撃に変わるわけだ。
「親」「子」と書くと字面的に強弱が逆のような感じがするので、「基底」「派生」と書く人もいる。
あくまで一例だが、これができる静的型付け言語は今の時代でも限られているのではないかと思う。
例えばGoやRust、Zigなどはこれができない。(といってもその言語が優れているかどうかということとは無関係だ。あくまでその言語がオブジェクト指向型言語かどうか、ということに過ぎない)
ちなみにCはオブジェクト指向型言語ではないくせにこれができる。
|