以前
http://d.hatena.ne.jp/kmaebashi/20071031#p1
にメモだけ書いた件をもうちょっとちゃんと書きます。
単一継承しか考えないのであれば、ポリモルフィズムは、オブジェクトごとに関数へのポインタ*1の配列(俗に言うvtable)へのポインタを持たせればよいわけです。そのへんの話は以前こちらとか「Java謎+落とし穴徹底解明」に書いたのですが、簡単に再掲すると、下図のように、オブジェクトの先頭あたりに、メソッドへのポインタを保持したテーブルを持たせます。これがvtableで、vtableへのポインタは、オブジェクトをnewした際に設定します。
ここでたとえば
- getX()というメソッドのvtable上の添字は0
- getY()というメソッドのvtable上の添字は1
ということさえ決まっていれば、オブジェクトのメソッドを呼び出す際は、vtableからメソッドの実体を引っ張り出して呼び出すことができますし、ポリモルフィズムが欲しければ、サブクラスではvtableに異なるメソッドのポインタを設定します。
ただし、この方法は単一継承であればこそうまくいくのであって、多重継承ではメソッドの添字が一意に決まらないためうまくいきません。そこで、C++では、アップキャストの際にこっそりポインタの値を変えるという豪快な解決法を取っています。たとえばAとBを継承したクラスCがあるとき、AとCは「メインの継承関係」として単一継承の際と同じ方法が取れますが、CがBにアップキャストされた際には、ポインタ自体を、Bのvtableがあるアドレスに変換します。
おかげでうっかりC++のオブジェクトを(Cの感覚で)void*のポインタで保持したりしてえらい目に遭った経験がありますが、それはさておき、C++の場合アップキャストのタイミングをコンパイラが完全に把握できるからこういうことができるわけです。
ではDiksamはどうかというと、Diksamも静的型付け言語なので、C++同様、ポインタのアドレスをこっそり変換することも可能です。しかし、DiksamにはGCがあるので、ポインタが、ヒープに確保されたメモリ領域の途中を指す、という状態は避けたいところです。GCのマークフェーズはオブジェクトごとに行われるため、ポインタが変なところを指していると扱いが面倒になります。
そこで、Diksamでは、値型(DVM_Value)に、オブジェクトへの参照とともにインタフェースのインデックスを持たせる、という方法を取りました。
具体的には、DVM_Valueは以下のような定義になっていて、
typedef union { int int_value; /* 整数型の場合 */ double double_value; /* 実数型の場合 */ DVM_ObjectRef object; /* オブジェクトの場合 */ int function_index; /* 関数へのポインタ(本題と関係ありません) */ }
このDVM_ObjectRefは
typedef struct { DVM_VTable *v_table; DVM_Object *data; } DVM_ObjectRef;
のようになっています。
オブジェクトを指すためのメンバdataと、vtableを指すためのメンバv_tableを分けて持ち、インタフェースへのキャストの際、v_tableを書き換えているわけです。
アレな方法かもしれませんが、今のところ動いているようです。ポインタ1個で、(そのへんの32bit環境で)8バイト食うのもアレですが、DVM_Valueはもともとdoubleを含む共用体なので一緒ですし…ってそういう問題でないのはわかってはいるのですが。
*1:ここでの「ポインタ」は必ずしも「アドレス」を意味しない