C++ メンバー関数ポインタの話

C++メンバー関数ポインタで仮想関数だとどうなるんだろ?と思って調べてみた。

class C {
public:
  virtual void func() { ... }
  typedef void (C::*funcptr)();
  funcptr fp;

  C() {
    fp = &C::func();
  }
};

このとき、fpはC::func関数のメモリアドレスと同じ値かと思っていたのですが、ぜんぜん違った。

実態はvtbl内のaddress offsetのような数値(実際はオフセット+1の数値)です。

vtblのオフセットなので、class Cを継承したDというクラスでfuncをoverrideするとメンバー関数ポインタfpを使った呼び出しはきちんと、overrideされたD::funcを呼び出します(コンストラクタの中ではvtblの関係でだめですけど)。

このソースコードで、C::func()が呼び出されないという不思議。

funcがvirtualではない場合、普通に関数アドレスが戻ってるので、呼び出し時にどちらか判断する必要があります。

ここに、なんらかの仕組みが必要なのでasmソース出して調べてみた。

movl   -12(%ebp), %eax
    movl    4(%eax), %eax
    andl    $1, %eax ← 仮想関数テーブルオフセットかの判断
    testl   %eax, %eax
    jne L4 ← 仮想関数テーブルだったらL4に飛ぶ
    movl    -12(%ebp), %eax
    movl    4(%eax), %eax
    jmp L5
L4:
    movl    -12(%ebp), %eax ← 仮想関数のアドレスを解決
    movl    8(%eax), %eax
    movl    %eax, %edx
    movl    -12(%ebp), %eax
    addl    %edx, %eax
    movl    (%eax), %edx
    movl    -12(%ebp), %eax
    movl    4(%eax), %eax
    decl    %eax ← 仮想関数オフセットのフラグを落とす
    addl    %edx, %eax
    movl    (%eax), %eax
L5:
    movl    -12(%ebp), %edx
    movl    8(%edx), %edx
    movl    %edx, %ecx
    movl    -12(%ebp), %edx
    addl    %ecx, %edx
    movl    %edx, %ecx
    call    *%eax

この通り(?)、通常の関数アドレスやvtblオフセットが32bit alignなのを利用して、関数アドレスの場合はそのまま、vtblオフセットの場合は+1しておくことで1ビット目で判断してました。

メンバー関数ポインタの呼び出しがオブジェクト内部からでも(this->*fp)();とする必要がある理由は、この処理にあるんすね。

それにしても仮想関数テーブルのlookupコードが長いので-O2してみた。

movl   4(%ebx), %eax
    movl    8(%ebx), %ecx
    testb   $1, %al
    je  L6
    movl    (%ebx,%ecx), %edx
    movl    -1(%edx,%eax), %eax
L6:
    addl    %ebx, %ecx
    call    *%eax

こんなもんか。ふむ。