ゆき社長

シーゲンガーのお勉強 ゲームプログラマ、ゲーマー、色々!

C++の仮想関数の仕組み&静的に呼ぶ方法

C++における関数のオーバーライド、ポリモーフィズム

知ってる人には常識だけど他のオブジェクト指向言語の人は悩む事が多い

baseクラスから subA subB クラスを派生させ

printメソッドをオーバーライドさせ

ポリモーフィズムで 実行させる例

class base

{

public:

void print()

{

printf( "base\n" );

}

};

class subA : public base

{

public:

void print()

{

printf( "subA\n" );

}

};

class subB : public base

{

public:

void print()

{

printf( "subB\n" );

}

};

base *cls[3];

cls[0] = new subA();

cls[1] = new subB();

cls[2] = new base();

for( int n=0; n<3; n++ )

{

cls[n]->print();

}

結果:

base

base

base

subA subBクラスで printをオーバーライドしたはずなのに

スーパークラスの printメソッドが呼ばれてしまう

アセンブラだと

mov eax,dword ptr [n]

mov ecx,dword ptr cls[eax*4]

call base::print (1C113Bh)

base::print を呼んでいるので 当然の動き。

問題は

base *cls[3];

C++では通常はメソッドをオーバーライドしても、呼び出したクラスのメソッドが呼ばれる

ポリモーフィズム的には

cls[0]->print();

は、subA のメソッドを呼んでほしい

そういう場合は メソッドに virtualをつける

class base

{

public:

virtual void print()

{

printf( "base\n" );

}

};

class subA : public base

{

public:

virtual void print()

{

printf( "subA\n" );

}

};

class subB : public base

{

public:

virtual void print()

{

printf( "subB\n" );

}

};

base *cls[3];

cls[0] = new subA();

cls[1] = new subB();

cls[2] = new base();

for( int n=0; n<3; n++ )

{

cls[n]->print();

}

結果:

subA

subB

base

これで正しく ポリモーフィズムが出来た。

なぜ スーパークラスのポインタ型なのに サブクラスのメソッドが呼べたかのからくりは

virtual method table(vtable)にある。

virtualで宣言するとそのメソッドは vtableが作られる

このvtableは virtualメソッドへの関数ポインタが入っていて

オーバーライドの際に、このvtableの値が サブクラスの関数ポインタへと置き換わる

その辺は 他のページみて下さい。

要は virtualでメソッドを宣言したら 関数アドレス直接ではなく、vtableを使った間接呼び出しになること

また vtableはnewした時に、サブクラスの関数アドレスが代入されること

ゆえに スーパークラスのポインタでcallしても サブクラスのメソッドが呼ばれる

アセンブラだとこう

cls[n]->print();

00E61712 mov edx,dword ptr [n]

00E61715 mov eax,dword ptr cls[edx*4]

00E61719 mov edx,dword ptr [ecx]

00E6171B mov esi,esp

00E6171D mov ecx,eax

00E6171F mov eax,dword ptr [edx]

00E61721 call eax

そして call eax の先(vtable)は

jmp subA::print (0BD19D0h)

vtableを使い 間接呼び出しを行っている

ここまでは C++使ってる人には常識だけど

この 間接呼び出しは遅い。

jmp命令が入るだけのようだが

コンパイラが静的に関数アドレスを解決出来ないため

コンパイラが最適化出来ないのが 痛い。

じゃあ どうやれば速くなるか・・・ という事ですが

まず 仮想関数を静的に呼ぶ方法から。

けっこう知らない人が多いので

色々と 表記ができます

まず先ほどの 愚直にCallする

cls[n]->print();

00E61712 mov edx,dword ptr [n]

00E61715 mov eax,dword ptr cls[edx*4]

00E61719 mov edx,dword ptr [ecx]

00E6171B mov esi,esp

00E6171D mov ecx,eax

00E6171F mov eax,dword ptr [edx]

00E61721 call eax

vtable 使ってますね。当然

subA クラスにキャストして呼んでみる

((subA*)cls[0])->print();

00BD16F3 mov eax,dword ptr [cls]

00BD16F6 mov edx,dword ptr [eax]

00BD16F8 mov esi,esp

00BD16FA mov ecx,dword ptr [cls]

00BD16FD mov eax,dword ptr [edx]

00BD16FF call eax

vtable 使ってますね。

dynamic_cast使って subAクラスにキャストしてみる

dynamic_cast(cls[0])->print();

0031174D push 0

0031174F push offset subA `RTTI Type Descriptor' (318018h)

00311754 push offset base `RTTI Type Descriptor' (318000h)

00311759 push 0

0031175B mov eax,dword ptr [cls]

0031175E push eax

0031175F call @ILT+660(___RTDynamicCast) (311299h)

00311764 add esp,14h

00311767 mov dword ptr [ebp-150h],eax

0031176D mov ecx,dword ptr [ebp-150h]

00311773 mov edx,dword ptr [ecx]

00311775 mov esi,esp

00311777 mov ecx,dword ptr [ebp-150h]

0031177D mov eax,dword ptr [edx]

0031177F call eax

vtableですね

dynamic_cast使って subAクラスにキャストしてみる 更に メソッドにスコープつける

dynamic_cast(cls[0])->subA::print();

00301708 push 0

0030170A push offset subA `RTTI Type Descriptor' (308018h)

0030170F push offset base `RTTI Type Descriptor' (308000h)

00301714 push 0

00301716 mov eax,dword ptr [cls]

00301719 push eax

0030171A call @ILT+660(___RTDynamicCast) (301299h)

0030171F add esp,14h

00301722 mov ecx,eax

00301724 call subA::print (30100Ah)

キター

でも 遅そうですね

結論はこうです。 subAにキャストしつつ、printにスコープをつける

((subA*)cls[0])->subA::print();

0032170E mov ecx,dword ptr [cls]

00321711 call subA::print (32100Ah)

これが チョッパヤ

もちろん 安全性等考えると dynamic_cast 使うべきです。

おまけ

cls[0]->base::print();

00BD1710 mov ecx,dword ptr [cls]

00BD1713 call base::print (0BD115Eh)

((subB*)cls[0])->subB::print();

00251718 mov ecx,dword ptr [cls]

0025171B call subB::print (2511D6h)

スーパークラスにキャスト出来るのはわかるが、スーパーじゃないsubBにもキャストできて

一見正しく動く事もある(もちろん 絶対ダメ!)

こういうミス防ぐためには 速度犠牲になるけどdynamic_cast 使うべきなんだなぁ・・・

ただ、スーパークラスメソッド呼べるのは場合によっては使うかもね。

知らない人はこの構文覚えるべし

しかし

((subA*)cls[0])->subA::print();

には問題がある。これは プログラマが subAである事を知っていないと成り立たない

つまり ポリモーフちゃんと出来ない。

if( typeid(cls[0]) == typeid(subA) )

{

((subA*)cls[0])->subA::print();

}

else if( typeid(cls[0]) == typeid(subB) )

{

((subB*)cls[0])->subB::print();

}else{

cls[0]->base::print();

}

こんな感じで RTTI情報を見て処理分ければ

速くできる可能性もある。ただし typeid の== 演算子がどうやら文字列比較してて

遅いので注意

結論から言うと ポリモーフィズムは普通にvtable使って呼ぶ

プログラム時に 型がわかっている時は

((subA*)cls[0])->subA::print();

こんな感じで vtableではなく、静的な関数アドレスを指定する