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

こんなもんか。ふむ。

One thought on “C++ メンバー関数ポインタの話

  1. Pingback: メモリ配置とキャスト – wizaman's blog

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.