オーバーロードの仕組みをサンプルで

オーバーライドの仕組みをサンプルで - しげるメモでオーバーライドをやったので、今回はオーバーロードについて自作してみます。
(一応)オーバーロードの仕組みを説明しておくと、Javaでは同じ名前の引数が異なるメソッドを同じクラスに複数定義できます。
以下、Overload.javaといういろいろなところで再利用するつもりのサンプルです。

public class Overload {
    public static void main(String[] args) {
        Overload object = new Overload();
        object.greetings();
        object.greetings("Overload!");
        return;
    }
    public void greetings() {
        System.out.println("Hello");
    }
    public void greetings(String message) {
        System.out.println(message);
    }
}

引数なしのgreetingsと、Stringを引数にとるgreetingsがオーバーロードで定義されています。これを実行すると、ちゃんと下のように表示されます。

Hello
Overload!

ちょっと前に「Javaオーバーロード解決は静的に行われる」と書きましたが、今回はこれをもう少し深く追っていく感じで。
まず、先ほどのOverload.javaをMyClassの仕組みで再現してみます。なお、クラスベースのオブジェクト指向システム(MyObjectとかMyClassとか)は前回のを流用します。
前回作ったMyClassとかにはオーバーロードの仕組みはありませんが、とりあえず同じ名前のメソッドを多重定義してみます。

MyClass Overload = new MyClass(
    null, // extends null 
    new MyMethod("greetings") { // greetings()
        @Override
        public MyObject invoke(MyObject self, MyObject... args) {
            new MyString("Hello").invoke("print");
            return null;
        }
    },
    new MyMethod("greetings") { // greetings(message)
        @Override
        public MyObject invoke(MyObject self, MyObject... args) {
            MyObject message = args[0];
            message.invoke("print");
            return null;
        }
    }
);

プログラムエントリはこんな感じで。引数なしと文字列を引数に取るgreetingsメソッドをそれぞれ呼び出しています。

// var object = new Overload();
MyObject object = Overload.newInstance();
// object.greetings();
object.invoke("greetings");
// object.greetings("Overload!");
object.invoke("greetings", new MyString("Overload!"));

で、これを実行するとクラッシュします。

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
    at com.example.oload.MyOverload$2.invoke(MyOverload.java:41)
    at com.example.MyObject.invoke(MyObject.java:27)
    at com.example.oload.MyOverload.main(MyOverload.java:50)

「object.invoke("greetings");」が引数ありのgreetingsメソッドに束縛されて、引数を取り出す際に落ちたようです。

これを解決する方法は簡単です。

メソッド名に引数情報を加えてやればいい。

MyClass Overload = new MyClass(
    null, // extends null 
    new MyMethod("greetings()") { // 引数型まで明記
        @Override
        public MyObject invoke(MyObject self, MyObject... args) {
            new MyString("Hello").invoke("print");
            return null;
        }
    },
    new MyMethod("greetings(String)") { // こっちも明記
        @Override
        public MyObject invoke(MyObject self, MyObject... args) {
            MyObject message = args[0];
            message.invoke("print");
            return null;
        }
    }
);

呼び出す際にも、自分がコンパイラになった気分で呼び出し先のメソッド名に型情報を加えていきます。

// var object = new Overload();
MyObject object = Overload.newInstance();
// object.greetings();
object.invoke("greetings()"); // 呼び出し時にも引数型を明記
// object.greetings("Overload!");
object.invoke("greetings(String)", new MyString("Overload!")); // こっちも

これでちゃんと実行できました。

Hello
Overload!

そりゃまぁ、できますよね。

というところですが、実は本家Javaでやってることも大差ありません。以下、javacで最初のサンプルをコンパイルしたものを、javap -cでディスアセンブルしたものに注釈をつけてみました。

$ javap -classpah bin -c Overload
Compiled from "Overload.java"
public class Overload extends java.lang.Object{
...
public static void main(java.lang.String[]);
  Code:
/* Overload a1 = new Overload(); */
   0:   new     #1; //class Overload
   3:   dup
   4:   invokespecial   #16; //Method "":()V
   7:   astore_1
/* overload.greetings(); */
   8:   aload_1
   9:   invokevirtual   #17; //Method greetings:()V
/* overload.greetings("Overload!"); */
   12:  aload_1
   13:  ldc     #20; //String Overload!
   15:  invokevirtual   #22; //Method greetings:(Ljava/lang/String;)V
/* return; */
   18:  return
...
}

「Method greetings:()V」と「Method greetings:(Ljava/lang/String;)V」が別にあるように、クラスファイルが生成された時点ではすでにオーバーロードメソッドの静的な解決が完了しています。このように、Javaコンパイル時にメソッドの名前と引数/戻り値型の情報をクラスファイルに埋め込んでいました。これらの情報をあわせてメソッドのシグネチャとか呼ぶらしいです。
前回のオーバーライドはオブジェクトが動的に解決していましたが、今度のオーバーロードコンパイラの層で静的に解決しちゃってます。

動的とか静的とかはメソッドオーバーロードの動的束縛とか - しげるメモあたりで。動的オーバーロードはそれはそれで問題がありますが。

ここからC++

いまいちテクニカルな感じがしなかったので、C++についても触れておきます。

C++はCと違って関数のオーバーロードができます。ここからはこの仕組みについて。
なお、移動中にこのエントリを書いてるのでコンパイラMinGWでやってます。あまりほめられた使い方をしないので、バージョンが違うと動きが違うかも。

$ g++ --version
g++ (GCC) 3.4.5 (mingw-vista special r3)
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

まず、C++でOverload.javaのようなものを書いてみました(overload.cpp)。C++書いたのが3年ぶりくらいなんで、変なところを見かけても全力でスルーしてください。

#import <iostream>
using namespace std;
void greetings();
void greetings(char *);
int main(int argc, char *argv[]) {
    greetings();
    greetings("Overload!");
}
void greetings() {
    cout << "Hello" << endl;
}
void greetings(char *message) {
    cout << message << endl;
}

ともあれ、これをコンパイルして動かすとちゃんと関数greetingsがオーバーロードされていることがわかります。

$ g++ gcc\overlod.cpp -o gcc\overload.exe
$ gcc\overload
Hello
Overload!

Cではオーバーロードが使えないので、関数プロトタイプに'extern "C"'するとコンパイルエラーになります

..error: declaration of C function `void greetings(char*)' onflicts with
..error: previous declaration `void greetings()' here
..: In function `int main(int, char**)':
..error: too few arguments to function `void greetings(char*)'
..error: at this point in file

greetingsがぶつかっているらしい。

GCCでのC++オーバーロードを調べるには、objdumpするのがいいと思います。objdump -tでオブジェクトコードのシンボルテーブルをダンプ出力できます。

$ g++ -c gcc\oveload.cpp -o gcc\overload.o
$ objdump -t gcc\overload.o
...
[  5].. 0x000000fe _main
[  6].. 0x00000140 __Z9greetingsv
[  7].. 0x0000016c __Z9greetingsPc
...

「__Z9greetingsv」とか「__Z9greetingsPc」とか出てきました。これはgreetings(void)とgreetings(char*)をそれぞれmangleした名前です。
それぞれ、最初のアンダースコアはMinGWが関数に共通してつけたもので、以降が"_Z[name-length][name][param-types]"の形式になっています*1。このテキストをc++filtに流してやれば、関数名と引数型を復元することができます。結局Javaのときと同じで、関数名だけじゃなくて引数の型一覧も含めてシグネチャとしています。
extern "C"の場合はこのようにならず、関数名そのものがシンボルテーブルに格納されます。その場合、引数型の情報は落ちてしまうためオーバーロードすることはできません。

少し慣れてくると、手動でオーバーロードの解決もできます。

#import <iostream>
using namespace std;
extern "C" void _Z9greetingsv(void);
extern "C" void _Z9greetingsPc(char *);
int main(int argc, char *argv[]) {
    _Z9greetingsv();
    _Z9greetingsPc("Overload?");
}
void greetings() {
    cout << "Hello" << endl;
}
void greetings(char *message) {
    cout << message << endl;
}

要は、「C++で定義された関数はmangleされて別の名前になる」「Cで定義された(extern "C")関数はmangleされずにそのまま利用される」ということを組み合わせています。つまり、mangleされて別の名前になったものを、extern "C"で直接指定しているだけ。

ちなみにちゃんと動きます。

$ g++ gcc\handovrload.cpp -o gcc\handoverload.exe
$ gcc\handoverload
Hello
Overload?

まとめ

*1:名前空間とか入れるともう少し変わります