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をオーバーライドしたはずなのに
アセンブラだと
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ではなく、静的な関数アドレスを指定する