C++ でオーバーライドした仮想関数をコンストラクタで呼ぶ

ClassA の仮想関数 method() を継承した ClassB でオーバライドした場合でも ClassA のコンストラクタ内の method() は ClassA::method() を指している。これは vptr の初期化タイミングで説明できると思っていたが違った orz

(以下、ubuntu 64bit 環境で試しているので vtbl などのポインタは 8 バイト。念のため -O0 でビルドしている)

class ClassA
{
public:
	ClassA()
	{
		method();
	}

	virtual void method()
	{
		printf("ClassA::method()\n");
	}
};

class ClassB : public ClassA
{
public:
	void method()
	{
		printf("ClassB::method()\n");
	}
};

まずは、C++ レベルで vptr/vtbl を表示してみる。

#include <cstdio>

void checkVTBL0andCall(void* ptr)
{
	long long* ptr_object = (long long*)ptr;
	printf("ptr_object = %llx\n", (long long)ptr_object);

	long long vptr = ptr_object[0];
	printf("vptr = %llx\n", (long long)vptr);

	long long* vtbl = (long long*)vptr;
	printf("vtbl[0] = %llx\n", (long long)vtbl[0]);

	void (*f)() = (void (*)())vtbl[0];
	f();

	return;
}

class ClassA
{
public:
	ClassA()
	{
		checkVTBL0andCall(this);
		method();
		method2();
	}

	virtual void method()
	{
		printf("ClassA::method()\n");
	}

	void method2()
	{
		method();
	}
};

class ClassB : public ClassA
{
public:
	void method()
	{
		printf("ClassB::method()\n");
	}
};

int main()
{
	ClassA* ptr = new ClassB;
	checkVTBL0andCall(ptr);
	ptr->method();
	ptr->method2();

    return 0;
}

シンボル情報は以下の通り。

$ objdump -Ct a.out | grep -e ClassA -e ClassB
0000000000400b88  w    O .rodata	0000000000000008              typeinfo name for ClassB
00000000004009d6  w    F .text	0000000000000025              ClassB::ClassB()
0000000000400984  w    F .text	0000000000000018              ClassA::method()
0000000000400b50  w    O .rodata	0000000000000018              vtable for ClassB
00000000004009be  w    F .text	0000000000000018              ClassB::method()
0000000000400946  w    F .text	000000000000003d              ClassA::ClassA()
0000000000400b70  w    O .rodata	0000000000000018              vtable for ClassA
0000000000400b90  w    O .rodata	0000000000000018              typeinfo for ClassB
000000000040099c  w    F .text	0000000000000021              ClassA::method2()
0000000000400ba8  w    O .rodata	0000000000000008              typeinfo name for ClassA
0000000000400bb0  w    O .rodata	0000000000000010              typeinfo for ClassA

実行結果は以下の通り。クラス A のコンストラクタの段階では method() は ClassA::method() を指しているが、生成後は ClassB::method() でオーバーライドされている。オブジェクトの vptr が指している vtbl が異なるのでそのせいかと思いきや・・・

ptr_object = 1f20010
vptr = 400b70ptr_object = 1352010
vptr = 400b80
vtbl[0] = 400984
ClassA::method()
ClassA::method()
ClassA::method()
ptr_object = 1352010
vptr = 400b60
vtbl[0] = 4009be
ClassB::method()
ClassB::method()
ClassB::method()

実際に ClassB::ClassB() の実装を見てみると、

  • ClassB 生成時に ClassB::ClassB() が呼び出される
  • ClassB() 内で基底のコンストラクタ ClassA::ClassA() が呼び出される
  • vptr を ClassB 用 (0x400b60) に切り替える

という処理をしていて、ClassA() 呼び出し時点では vptr はまだ書き換わっていない。これは予想通りだが、

00000000004009d6 :
  4009d6:	55                   	push   %rbp
  4009d7:	48 89 e5             	mov    %rsp,%rbp
  4009da:	48 83 ec 10          	sub    $0x10,%rsp
  4009de:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  4009e2:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  4009e6:	48 89 c7             	mov    %rax,%rdi
  4009e9:	e8 58 ff ff ff       	callq  400946  // ★ClassA のコンストラクタに呼び出し
  4009ee:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  4009f2:	48 c7 00 60 0b 40 00 	movq   $0x400b60,(%rax) // ★vptr を ClassB 用に切り替え
  4009f9:	c9                   	leaveq 
  4009fa:	c3                   	retq   

ClassA() の実装を見ると、vtbl 以前に普通に ClassA::method() が呼び出されていた!コンパイラ賢い!(method2() では vtbl を使ってジャンプしているのでこちらは vtbl の書き換えタイミングで説明できる。)

0000000000400946 :
  400946:	55                   	push   %rbp
  400947:	48 89 e5             	mov    %rsp,%rbp
  40094a:	48 83 ec 10          	sub    $0x10,%rsp
  40094e:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  400952:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  400956:	48 c7 00 80 0b 40 00 	movq   $0x400b80,(%rax)
  40095d:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  400961:	48 89 c7             	mov    %rax,%rdi
  400964:	e8 db fe ff ff       	callq  400844 
  400969:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  40096d:	48 89 c7             	mov    %rax,%rdi
  400970:	e8 0f 00 00 00       	callq  400984  // ★あれ・・・?
  400975:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  400979:	48 89 c7             	mov    %rax,%rdi
  40097c:	e8 1b 00 00 00       	callq  40099c 
  400981:	c9                   	leaveq 
  400982:	c3                   	retq   
  400983:	90                   	nop