最初に、戻り値もなく引数もとらないメンバ関数のコールバックを試みます。つまり次のようなコードが正常に動作することを目標とします。
#include <iostream> using namespace std; class ClassZ { public: void FunctZ( void ) { cout << "ClassZ::FunctZ" << endl; } }; int main( void ) { ClassZ z; void *pFunct = ConvertMethod( &ClassZ::FunctZ ); Invoke( &z, pFunct ); // equals z.FunctZ(); return 0; }
ユーザー定義関数 ConvertMethod を使って関数ポインタを汎用ポインタに無理矢理変換し、非タイプセーフながら、Invoke を使ってオブジェクトの非静的メンバ関数を呼び出す、というわけです。
第一に ConvertMethod をどう定義すればよいかを考えます。あらゆるクラスのメンバ関数を変換できるようにするのが望ましいため、引数の型としては汎用的に扱えるものがよいのですが、ご存知の通り、メンバ関数を指すポインタを、他の汎用的な型に直接変換することはできません。C++ のオーバーロードの機能を利用して、各クラスごとに専用の ConvertMethod を用意する方法もありますが、クラスが増えると面倒です。そこでもう一つの C++ の機能であるテンプレートを利用することにします。
template< typename MethodType > __declspec( naked ) void *__stdcall ConvertMethod( MethodType method ) { __asm { mov eax, dword ptr[ esp + 4 ] ; EAX = method; ret 4 ; return EAX; } }
ConvertMethod は次のように使用します。
int main( void ) { void *pvFunctZ = ConvertMethod( &ClassZ::FunctZ ); return 0; }
幾つか C++ では定義されていないキーワードが使用されているので、MSDN ライブラリなどを見れば書いてあるとは思いますが簡単に説明します。二行目の __declspec( ) とはマイクロソフト独自の拡張属性構文であり、その中で指定された属性が、続いて宣言されるオブジェクトや関数に適用されます。naked として宣言された関数に対し、コンパイラはプロローグ コードとエピローグ コードを生成しません。つまり、EBP レジスタの退避と復元、及び、ローカル変数のためのスタック領域の確保と解放を行なわないので、スタックを直接変更するような特殊な操作が容易になるし、若干の高速化も期待できます。次の __stdcall は、その関数で Pascal 呼び出し規約を使用することを指定するものです。標準の C 呼び出し規約との違いは、引数のためのスタック領域のクリアを、関数の呼び出し側ではなくて関数の側で行なうことです。それにより、可変個の引数は使用できなくなりますが、わずかながら高速になります。関数内部の __asm は、それに続くブロックの内部が、アセンブリ命令であることを指定するものです。これを使うと、C/C++ のソースコードの中にアセンブリ言語で書かれたコードを埋め込むことができます(インライン アセンブラといいます)。ブロックではなくて単一行構文として使うこともできます。x86 のアセンブリ命令の詳細については、インテルのホームページなどを参照してください。
話を元に戻して、ConvertMethod はこれで完璧でしょう。高速化の余地はありますが、今回は「動かすこと」が目的なのでそこまで追求しないことにして、次に Invoke について考えましょう。Invoke は次のように宣言することにします。
void Invoke( void *obj, void *funct );
obj はオブジェクトへのポインタで、funct は、先ほどの ConvertMethod を使って得られる、メンバ関数を指す汎用ポインタです。この二つを使って、
obj->*funct();
のようなことをしてみようというわけですが、上のコードをそのまま書いてもコンパイラは認めてくれませんので(また、それは無理な話です)、自前でアセンブリ コードに変換することにします。さてどのようなコードを書けばよいのでしょうか。それに答えてくれるのが、まさしく「コンパイラ」です。つまり z.FunctZ() などのような非静的メンバ関数に対してコンパイラが生成するコードを解析し、真似ればどうにかなるという寸法です。特に VC++ ならば「混合モード」という便利なデバッグ機能があるので、わざわざ逆アセンブラを使う必要もありません。アセンブリ命令(またはオブジェクト コードに変換された C/C++ のコード)の一つ一つをステップ実行できるので、私はインライン アセンブラを使用するプログラムを書くとき、いつもお世話になっています。コンパイラが生成したコードを見ると、クラスの非静的メンバ関数を呼び出すときには、次のステップを踏むようです。
1, 引数を、右から左の順に PUSH していく
2, ECX レジスタに、オブジェクトのアドレスを代入する
3, CALL 命令を使って、メンバ関数にジャンプする
ただし、これは引数の数が決まっているメンバ関数の場合であり、可変個の引数をとる場合は少し異なります。今回は引数のない関数だから 1 はなく、2 と 3 だけを行なえばよいことになります。即ち、Invoke の定義は次のようになります。
__declspec( naked ) void __stdcall Invoke( void *obj, void *funct ) { __asm { mov ecx, dword ptr[ esp + 4 ] ; ECX = obj; call dword ptr[ esp + 8 ] ; funct(); ret 8 ; return; } }
この Invoke を使えば、次のようにして当初の目標を完全に実現できます。
int main( void ) { ClassZ z; void *pvFunctZ = ConvertMethod( &ClassZ::FunctZ ); Invoke( &z, pvFunctZ ); return 0; } /* 実行結果 ClassZ::FunctZ */
あとはオブジェクトのアドレスと関数ポインタをセットにしたクラスか構造体をつくり、それをリストにすれば、デリゲートを実装したことになります。次回は、まず引数の数が固定されている普通のメンバ関数について説明し、それで記事の量が足りなければ(毎回 5KB 程度になるようにしています。あまり長ければ読む気が失せるでしょうし、それに私も HTML にするとき面倒ですから)、引数が可変個のメンバ関数について説明します。