なんやかんやでかろうじて連載も4回目になりまして、モチベーションも維持できてよかったです。語学留学もあと一カ月で、最近、仲良くなった大学生にC++を教えています。といっても最初はポインタを教えていたのですが次にリファレンスを教えることになって、思わず、「混乱の元だからリファレンスは最初は無理して覚えなくてよくてポインタを先に覚えましょう。」と言ってしまった。もっとも「リファレンスの方が簡単だから軽く覚えてガッツリポインタを勉強しよう。」とも言ったが、この連載のターゲットは『オブジェクト指向しか知らない世代』としていますが、JavaやC#しか知らない方たちはポインタの概念が理解できているか怪しい限りです。興味深いのは、私の半径3メートルの範囲、クパチーノ近辺ではC++をマスターすることがステータスになっているようです。
という訳(?)で今回はメソッドと関数について話します。なぜか知らぬがネットの界隈ではどうも、
メソッド>>>どうしようもない壁>>>関数
という図式が成り立っているようです。でこのどうしようもない壁なんて無いというのが今回のテーマです。もっとも1回で説明するのは難しいので何回かに分けて説明します(しかも飛び飛びになるかも)。
先ず、最初に以下の2つの表現
object.function();
function(object);
ですが、C++でみると生成される機械語コードは同じになります。例外はfunctionが仮想関数(ややこしいので以下、オーバーライドメソッドと呼びます)の場合になります。オーバーライドメソッドを除いて、C++でみると上記の2つのコードは同じということになります。『カプセル化があるだろ』という突っ込みがあるかもしれませんが、それは他のキーワード(friend)を使うことによって同等にできます。
もし、あなたには『どうしようもない壁』が見えるというのならどうぞ具体例とともにコメントをください。
ということで、ADPでは上記の2つのコードは同じように解釈されます。というか上のコードは下のコードとして解釈されます。『同じなら一つの書き方で良いだろ!』とお叱りを受けそうですが、表記上の重要な違いがあります。つまりobject.function()の記述はメソッドチェーンを実現できます。
object.function1().function2().function3();
もしこれを関数の形式で書くと
function3(function2(function1(object)));
となりますが、どちらが良いかは一目瞭然だと思います。ただ、これはあくまでも表記上の話でもしこれがどうしようもない壁というのならオブジェクト指向というのは表記上の問題ということで話が付きますし、そもそもメソッドチェーンの見た目はもはやメッセージパラダイムとは異なるでしょう。
さてfunctionがオーバーライドメソッドの場合の話ですが、簡単にオーバーライドメソッドについて話ますと、呼び出されるメソッドが実行時に決定される点です。C言語の関数は呼び出される実体がコンパイル時に決まりますが、オーバーライドメソッドの場合は実行時にオブジェクトの型によって呼び出される実体が決まります。
object.add(1);
とあった場合、objectがInteger型ならIntegerクラスのaddが呼び出され、objectがFloat型ならFloatクラスのaddが呼び出されるということになります。これはポリモーフィズムと呼ばれるもので、オブジェクト指向病にかかっている人たちはまさにポリモーフィズムが手続き型言語との差別化を図っていると信じています。ポリモーフィズムについては連載の後の方で詳しく説明しますが、関数ポインタを使うとこによりCやアセンブリ言語でも同様の機能を実現できます。こう言うと『ならアセンブリ言語で全部書けや!』と意味不明な切り返しをされるのですが、それに答ますとLinuxの記述では主にCが使われているというのは有名な話ですが、でデバイスドライバの実装等ポリモーフィズムと同様のことが行われています。つまりCでもある程度はオブジェクト指向を実用レベルでシミュレートできるということです。ちなみにADPはC++で記述しておりポリモーフィズムもバリバリ使っていますがパフォーマンス上の理由からCでリライトしようかと思案中です。
さて、話が横道にそれましたので元に戻しますとaddの例ですが、何か変なことに気付かないでしょうか?例えば以下の例はどうでしょうか?
object1.add(object2); // ・・・・・(A)
このaddメソッドですが、object1に対してはポリモーフィズムが効きますがobject2に対してはどうでしょうか?JavaやC++ではobject2にはポリモーフィズムは働きません。まぁ関数の例でもそうでしたが引数にはポリモーフィズムは働かないと考えてもしようがない面もありますが、もし引数にポリモーフィズムが働かないのが当然だというのなら(A)は以下の例と動作が異なることになります。
object2.add(object1); // ・・・・・(B)
加算(add)というのは通常交換法則が成り立ちますのでもし(A)と(B)の動作が異なるということならその実装は不完全としか言いようがないですね。ので通常addがポリモーフィズムであるならそれはobject1,object2双方に対してポリモーフィズムでなければならないことになります。2つのオブジェクトに対してポリモーフィズムを働かせることをダブルディスパッチと呼びます。私はダブルディスパッチについて16年程前に『More Effective C++』で知ったのですが、以前、といっても2,3年程前にJavaが得意で自称オブジェクト指向をマスターしている彼が『オブジェクト指向をマスターしている人は日本にはほとんどいない。俺を除いて』と言っていたので『ダブルディスパッチって知っている?』と聞いたら『なにそれ?』と返されたことがある。ということで知ったかのJava厨の検出にはダブルディスパッチは今のところ効果的かと思われる。ちなみにC#4.0以降の場合、dynamicキーワードを使うことにより(A)の記述でもobject2に対してもポリモーフィズムを働かせることができるのでC#プログラマにダブルディスパッチの技を繰り出すのは返り討ちにあう可能性があるので注意したい。話を戻してC++やJavaの場合はかの有名なデザインパターンの一つビジターパターンがダブルディスパッチの実装方法の一つとなる。
ダブルディスパッチの使いどころですが、二項演算子でポリモーフィズムを使いたいとき(もっともパフォーマンス上の問題があり二項演算子でポリモーフィズムは使わない場合が多い)や2つの物体の衝突を計算する場合(More Effective C++)があるでしょう。ADPではユニフィケーションの実装にビジターパターンによるダブルディスパッチを使用しています(これがしたいが為にC++を使ったがもっと効率的な記述をしたいからCで組みなおそうかと考えている)。
つまりこういうことになる。
object.method()
と
function(object)
において表現上の違いやプログラミング言語のサポート状況による違いはあるが本質的には両者を同等物とみて構わないということである。つまりどうしようもない壁はないということになる。
長くなったので続きは来週に持ち越します。