What is the virtual table table?

The other day on Slack, I learned a new acronym for my C++ acronym glossary: “VTT.” Godbolt:

test.o: In function `MyClass':
test.cc:3: undefined reference to `VTT for MyClass'

“VTT” in this context stands for virtual table table. It’s an auxiliary data structure used (in the Itanium C++ ABI) during the construction of some base classes which themselves have virtual bases. It follows the same placement rules as the class’s vtable and typeinfo; so if you’re getting the above error, you can simply imagine that it said “vtable” instead of “VTT” and debug accordingly. (Most likely, you left the class’s key function undefined.) To see why the VTT — or something like it — is needed, let’s start with the basics.

Order of construction for non-virtual bases

When we have an inheritance hierarchy, base classes are constructed from the basest subobjects downward. To construct a Charlie, we must first construct his parent classes MrsBucket and MrBucket; recursively, to construct MrBucket, we must first construct his parent classes GrandmaJosephine and GrandpaJoe.

That is:

struct A {};
struct B : A {};
struct C {};
struct D : C {};
struct E : B, D {};

// Constructor bodies run in the order
// A B C D E

Order of construction for virtual bases

But virtual bases mess up the order a little bit! With virtual bases, we might have a diamond hierarchy, where two different parent classes share a single instance of a grandparent class.

struct G {};
struct M : virtual G {};
struct F : virtual G {};
struct E : M, F {};

// Constructor bodies run in the order
// G M F E

In the previous section, each constructor was responsible for calling the constructors of its own base class subobjects. But now that we’re doing virtual inheritance, the constructors of M and F must somehow know not to construct their subobject of type G, because that subobject is shared. If M and F were responsible for constructing their own virtual base subobjects, we’d end up constructing that shared subobject twice in this case, and that would be bad.

So, to deal with virtual base subobjects, the Itanium C++ ABI splits each constructor into two parts: a base object constructor and a complete object constructor. The base object constructor is responsible for constructing all of the object’s non-virtual base subobjects (and its member subobjects, and setting its vptr to point to its vtable; and running whatever code is inside the curly braces in your C++ code). The complete object constructor, which is called whenever you create a complete C++ object, is responsible for constructing all of the most derived object’s virtual base subobjects and then doing all the rest of that stuff too.

Observe the difference between our A B C D E example from the previous section and the following example:

struct A {};
struct B : virtual A {};
struct C {};
struct D : virtual C {};
struct E : B, D {};

// Constructor bodies run in the order
// A C B D E

The complete object constructor for E first calls the base object constructors of virtual subobjects A and C; then it calls the base object constructors of non-virtual bases B and D. B and D are no longer responsible for constructing A and C respectively.

Construction vtables

Suppose we have a class with some virtual methods, like this (Godbolt):

struct Cat {
    Cat() { poke(); }
    virtual void poke() { puts("meow"); }
};

struct Lion : Cat {
    std::string roar = "roar";
    Lion() { poke(); }
    void poke() override {
        roar += '!';
        puts(roar.c_str());
    }
};

When we are constructing a Lion, we start by constructing its Cat base subobject. The Cat constructor calls poke(). At this point we have only a Cat object — we have not yet initialized the data members that are necessary to make it a Lion. If Cat’s constructor called Lion::poke(), it would try to modify an uninitialized std::string roar and we’d get UB. So the C++ standard mandates that inside Cat’s constructor, calling a virtual method such as poke() must call Cat::poke(), not Lion::poke()!

This is no problem. The compiler simply makes sure that Cat::Cat() (both the base-object and complete-object versions) starts by setting the object’s vptr to point to vtable for Cat. Lion::Lion() will call Cat::Cat(), and then reset the vptr to point to vtable for Cat-in-Lion before running the code in the curly braces. No problem at all!

Virtual base offsets

Suppose Cat has a virtual base Animal. Then vtable for Cat holds not just function pointers to Cat’s virtual member functions, but also the offset of Cat’s virtual Animal subobject. (Godbolt.)

struct Animal {
    const char *data = "hi";
};

struct Cat : virtual Animal {
    Cat() { puts(data); }
};

struct Nermal : Cat {};

struct Garfield : Cat {
    int padding;
};

Cat’s constructor asks for this Cat’s Animal::data member. If this Cat object is a base subobject of a Nermal object, then its data member lives at offset 8, just beyond the vptr. But if this Cat object is a base subobject of a Garfield object, then its data member lives at offset 16 — beyond the vptr and Garfield::padding. To deal with this situation, the Itanium ABI stores virtual base offsets in the vtable for Cat. The vtable for Cat-in-Nermal stores the fact that the Cat’s base Animal subobject is located at offset 8; the vtable for Cat-in-Garfield stores the fact that the Cat’s base Animal subobject is located at offset 16.

Now combine this with the previous section. The compiler must make sure that Cat::Cat() (both the base-object and complete-object versions) starts by setting the object’s vptr to point to vtable for Cat-in-Nermal or vtable for Cat-in-Garfield, depending on the type of the most derived object! How on earth does this work?

Well, the most derived object’s complete object constructor must precompute which vtable it wants the base subobject’s vptr to be pointed at during construction, and then the MDO’s COC must pass that information down into the base subobject’s base object constructor as a hidden parameter! Look at the codegen for Cat::Cat() now (Godbolt):

_ZN3CatC1Ev: # complete object constructor for Cat
  movq $_ZTV3Cat+24, (%rdi)  # this->vptr = &vtable-for-Cat;
  retq

_ZN3CatC2Ev: # base object constructor for Cat
  movq (%rsi), %rax  # fetch a value from rsi
  movq %rax, (%rdi)  # this->vptr = *rsi;
  retq

The base object constructor takes not just a hidden this parameter in %rdi, but also a hidden “VTT” parameter in %rsi! The base object constructor loads an address from (%rsi) and stores that address into the vtable of the Cat object.

Whoever calls the base object constructor of Cat is responsible for precomputing what address Cat::Cat() ought to store in its vptr, and setting (%rsi) to point to that address.

Why the extra indirection?

Look at Nermal’s complete object constructor.

_ZN3CatC2Ev: # base object constructor for Cat
  movq (%rsi), %rax  # fetch a value from rsi
  movq %rax, (%rdi)  # this->vptr = *rsi;
  retq
_ZN6NermalC1Ev: # complete object constructor for Nermal
  pushq %rbx
  movq %rdi, %rbx
  movl $_ZTT6Nermal+8, %esi    # %rsi = &VTT-for-Nermal
  callq _ZN3CatC2Ev            # call Cat's base object constructor
  movq $_ZTV6Nermal+24, (%rbx) # this->vptr = &vtable-for-Nermal
  popq %rbx
  retq
_ZTT6Nermal:
  .quad _ZTV6Nermal+24         # vtable-for-Nermal
  .quad _ZTC6Nermal0_3Cat+24   # construction-vtable-for-Cat-in-Nermal

Why does it “spill” _ZTC6Nermal0_3Cat+24 into the data section and pass its address in %rsi, instead of simply passing _ZTC6Nermal0_3Cat+24 directly?

# Why not just this?

_ZN3CatC2Ev: # base object constructor for Cat
  movq %rsi, (%rdi)  # this->vptr = rsi;
  retq
_ZN6NermalC1Ev: # complete object constructor for Nermal
  pushq %rbx
  movq %rdi, %rbx
  movl $_ZTC6Nermal0_3Cat+24, %esi # %rsi = &construction-vtable-for-Cat-in-Nermal
  callq _ZN3CatC2Ev                # call Cat's base object constructor
  movq $_ZTV6Nermal+24, (%rbx)     # this->vptr = &vtable-for-Nermal
  popq %rbx
  retq

Well, that’s because we might have several levels of inheritance here! At each level of inheritance, the base object constructor needs to set the vptr to something, and then possibly relay instructions further up the chain so that the more-base constructors can set their own vptrs to something(s) else. This implies a whole list or table of vtable pointers.

Here’s a concrete example (Godbolt):

struct VB {
    int member_of_vb = 42;
};
struct Grandparent : virtual VB {
    Grandparent() {}
};
struct Parent : Grandparent {
    Parent() {}
};

struct Gretel : Parent {
    Gretel() : VB{1000} {}
};
struct Hansel : Parent {
    int padding;
    Hansel() : VB{2000} {}
};

The base object constructor of Grandparent must set its vptr to point to Grandparent-in-whatever the most derived class is. The base object constructor of Parent must first call Grandparent::Grandparent() with a suitable %rsi, and then set its vptr to point to Parent-in-whatever the most derived class is. So the way Gretel implements this is:

Gretel::Gretel() [complete object constructor]:
  pushq %rbx
  movq %rdi, %rbx
  movl $1000, 8(%rdi) # imm = 0x3E8
  movl $VTT for Gretel+8, %esi
  callq Parent::Parent() [base object constructor]
  movq $vtable for Gretel+24, (%rbx)
  popq %rbx
  retq
VTT for Gretel:
  .quad vtable for Gretel+24
  .quad construction vtable for Parent-in-Gretel+24
  .quad construction vtable for Grandparent-in-Gretel+24

You can see in the Godbolt that Parent’s base object constructor first calls Grandparent::Grandparent() with %rsi+8, and then sets its own vptr to (%rsi). So it’s actually using the fact that Gretel has carefully laid out a trail of breadcrumbs, so to speak, which can be followed by all her base classes during construction.

The same VTT is used during destruction (Godbolt).

As far as I know, the zeroth entry in the VTT is never needed. Gretel’s constructor does load vtable for Gretel+24 into the vptr, but it knows that address statically; it never needs to load it from the VTT. I think the zeroth entry is probably just an accident of history. (And of course the compiler can’t just omit the zeroth entry these days, because that would violate the Itanium ABI and prevent linking against older, Itanium-ABI-following code.)

There you have it: the scoop on the virtual table table, or VTT.

For more information

You can find further information about the VTT in these places:

Finally, I should repeat that the VTT is a feature of the Itanium C++ ABI, as used on Linux, OSX, etc. The MSVC ABI used on Windows doesn’t have anything called the “VTT” as far as I’m aware, and uses a fairly different mechanism for virtual bases. I know next to nothing about the MSVC ABI (so far), but maybe one day I’ll figure it out and write a blog post about it!

Posted 2019-09-30