Multiple Vtables ad VPointers in C++

192 views Asked by At

I have been reading on vtables and pointers, but there is a few questions I still have. For example:

#include <iostream>

using namespace std;

class A
{
 public:
  virtual void PrintA()=0;  //1 vtable and 1 vpointer
};

class B
{
 public:
  virtual void PrintB()=0; //1 vtable and 1 vpointer
};

class Parent: public A, public B
{
 public:
  void PrintA();
  void PrintB();                // 3 vtables and 3 vpointers, right?
  virtual void PrintChild()=0;
};


void Parent::PrintA()
{
 cout<<"A";
}

void Parent::PrintB()
{
 cout<<"B";
}


class Child: public Parent
{
 public:
   void PrintChild(); //3 vtables and 3 vpointers
};

void Child::PrintChild()
{
 cout<<"Child";
}

int main()
{

  Parent* p1= new Child();
  p1->PrintChild();
  delete p1;
  return 0;
}

Question 1: Will Parent and Child will have 3 vtables and 3 vpointers?

Question 2: How will p1 know which vpointer to use? I heard it is up to the compiler but I just want someone clarify.

1

There are 1 answers

0
Tom Tromey On

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.