Yes, the definitive answers are compiler dependent. There isn't even a guarantee that virtual dispatch will be implemented with vtables.
Often a compiler will follow a particular platform's ABI. On many systems GCC implements a particular ABI that was invented for IA-64, but then was ported to other systems. This is readily available online, there are links from the GCC web site.
One way to see more about vtables in practice, on Linux at least, is to use gdb
, compile a small example program with -g
, and use info vtbl
to explore the vtable. However, this is a bit tricky at present due to a GCC bug involving debug info for virtual destructors; so just be sure to always have methods other than destructors.
I compiled your program and stopped in gdb
after p1
was initialized. Then:
(gdb) info vtbl p1
vtable for 'Parent' @ 0x400a10 (subobject @ 0x602010):
[0]: 0x400806 <Parent::PrintA()>
[1]: 0x400810 <Parent::PrintB()>
[2]: 0x400824 <Child::PrintChild()>
vtable for 'B' @ 0x400a38 (subobject @ 0x602018):
[0]: 0x40081a <non-virtual thunk to Parent::PrintB()>
Here you an see that Parent
and Child
actually just have two vtables, not three. This is because in this ABI, single inheritance is implemented by extending the parent class' vtable; and in this case, the extension of A
is treated this same way.
As to how p1
knows which vtable to use: it depends on the actual type that is used to make the call.
In the code, p1->PrintChild()
is called, and p1
is a Parent*
. Here, the call will be made via the first vtable you see above -- because nothing else makes sense, as PrintChild
isn't declared in B
. In this ABI, the vtable is stored in the first slot of the object:
(gdb) p *(void **)p1
$1 = (void *) 0x400a10 <vtable for Child+16>
Now, if you changed your code to cast p1
to a B*
, then two things would happen. First, the raw bits of the pointer would change, because the new pointer would point to a subobject of the full object. Second, this subobject would have its vtable slot pointing to the second vtable mentioned above. In this scenario, sometimes a special offset is applied to the subobject to find the full object again. There are also some special tweaks that apply when virtual
inheritance is used (this complicates object layout a bit, because the superclass in question only appears once in the layout).
You can see these changes like this:
(gdb) p (B*)p1
$2 = (B *) 0x602018
(gdb) p *(void**)(B*)p1
$3 = (void *) 0x400a38 <vtable for Child+56>
This is pretty much all specific to the ABI commonly used on Linux. Other systems may make different choices.