Search code examples
c++g++vtable

Can this change in subclass require recompilation of code dependent on superclass?


I have been learning some more "indepth" things about virtual tables recently and this question came to my mind.

Suppose we have this sample:

class A {
 virtual void foo();
}

class B : public A {
 void foo();
}

In this case from what I know there will be a vtable present for each class and the dispatch would be quite simple.

Now suppose we change the B class to something like this:

class B : public C, public A {
  void foo();
}

If the class C has some virtual methods the dispatch mechanism for B will be more complicated. There will probably be 2 vtables for both inheritance paths B-C, B-A etc.

From what I've learned so far it seems that if there would be somewhere else in the codebase function like this:

void bar(A * a) {
  a->foo();
}

It would need to compile now with the more complicated dispatch mechanism because at compile time we do not know if "a" is pointer to A or B.

Now to the question. Suppose we added the new class B to our codebase. It doesn't seem likely to me that it would require to recompile the code everywhere where the pointer to A is used.

From what I know the vtables are created by the compiler. However is it possible that this fixup is solved by the linker possibly during relocation? It does seem likely to me I just can not find any evidence to be sure and therefore go to sleep right now :)


Solution

  • Inside void bar(A * a), the pointer is definitely to an A object. That A object may be a subobject of something else like a B, but that's irrelevant. The A is self-contained and has its own vtable pointer which links to foo.

    When the conversion from B * to A * occurs, such as when bar is called with a B *, a constant offset may be added to the B * to make it point to the A subobject. If the first thing in every object is the vtable, then this will also set the pointer to the A vtable as well. For single inheritance, the adjustment is unnecessary.

    Here is what memory looks like for a typical implementation:

    | ptr to B vt | members of C | members of B | ptr to AB vt | members of A |
    
    B vt: | ptrs to methods of C (with B overrides) | ptrs to methods of B |
    AB vt: | ptrs to methods of A (with B overrides) |
    

    (Note that typically the AB vt is really still part of the B vt; they would be contiguous in memory. And ptrs to methods of B could then go after ptrs to methods of A. I just wrote it this way for formatting clarity.)

    When you convert a B * to an A *, you go from this:

    | ptr to B vt | members of C | members of B | ptr to AB vt | members of A |
    ^ your B * pointer value
    

    to this:

    | ptr to B vt | members of C | members of B | ptr to AB vt | members of A |
                                                ^ your A * pointer value
    

    Using a static_cast from A * to B * will move the pointer backwards, in the other direction.