C++ ABI for IA-64: Data Layout

Jason Merrill jason at cygnus.com
Thu Aug 12 05:29:44 UTC 1999


The offset to top field is required for dynamic_cast<void*>, which is
supported for all polymorphic classes.  So yes, it's always needed.

The only replicated entries which would be useful are ones from virtual
bases; we can get to entries from secondary vtables in non-virtual base
subobjects directly, since we're concatenating them.

In Category 3, why do you say "base classes are not empty or nearly empty"?
We still need offsets for empty virtual bases, since they might be at a
different location in the complete object.  Why does it matter if the base
classes have bases themselves?

It seems to me that we don't have to duplicate base offset info that's
already in one of our secondary vtables.

----------------
Further analysis:

This description is pretty good, but it doesn't describe how it's used.
I don't see how it can work without relying on concatenation.  Consider:

  struct A {
    virtual void f () = 0;
  };

  struct B {
    virtual void g () = 0;
  }

  struct C: public A, public B {
    void f () { ... }
    void g () { ... }
  };

  int main () 
  {
    C c;
    B* bp = &c;
    bp->g();
  }

Here, in the call to g, we must adjust 'this' to point to the C object.
But C is not a base of B, so B's vtable doesn't have the offset we want.
The vtable that has the interesting offset is C's.  Fortunately, since
we're concatenating, and in the callee we know what our dynamic type is
(because the dynamic type determines the entry point), we can look down
into C's vtable to find the offset.  So far, so good.

Is this how Taligent does/did it?  If not, how?

The situation gets more complicated with virtual inheritance.  Consider:

  struct A {
    virtual void* f () = 0;
    virtual void* g () = 0;
  };

  struct B: virtual public A {
    void* f () { return this; }
    int i;
  };

  struct C: virtual public A {
    void* g () { return this; }
    int j;
  };

  struct D: public B, public C { };

  int main ()
  {
    D d;
    A* ap = &d;
    B* bp = &d;
    C* cp = &d;

    return (ap->f() != bp || ap->g() != cp);
  }

The offset from A to B is different depending on whether the complete
object is a B or a D.  Furthermore, the offset from the A vptr to the B
vtable is different, so we can't just reuse the f entry in A's vtbl that we
inherit from B; it would look for the offset in the wrong place.  So what
do we do?  The only option I can see is to generate an adjustment thunk
that looks in the right place, but that would have the same problems as the
traditional thunking approach; namely, indirect jumps.

Does anyone see a way to avoid the 3rd-party thunk in this case?

One possibility would be to put derived, rather than base, class offsets in
the vtable; perhaps that's what Taligent really does.  This would have the
effect that A's vtable would be larger in a B than in an A.  Further
derivation could cause it to grow still further.  As a result, we wouldn't
be able to take advantage of concatenation to access secondary vtables
through the primary vptr for a class, since we wouldn't know the size of
the base vtables.

Another possibility would be to fall back on ARM-style vtables, with the
offset in the entry.  This option produces larger vtables, but does not
impede optimization.

Jason




More information about the cxx-abi-dev mailing list