C++ / a working model

25 / 163   ·   C++11   ·   9 min

Vtables: Language Semantics and ABI Implementation

Keep this sentence

The standard specifies which final overrider a virtual call should select, but it does not specify the number of vtables, the location of a vptr, or the object's memory map. Common ABIs implement those semantics with vtables, adjustment thunks, and RTTI metadata. Understanding them helps debugging, but you must not verify them with unguaranteed memory reads.

In this lesson
  1. State the behavior the standard guarantees first
  2. How a common ABI supports multiple inheritance
  3. Verify behavior; do not probe unguaranteed bytes
  4. Example
  5. Exercise

State the behavior the standard guarantees first

When a virtual function is called through a base-class pointer or reference, the final overrider corresponding to the actual object is usually executed. Explicitly qualified calls and the construction and destruction phases have extra rules. The standard requires this observable behavior. It does not require that the object header contain a pointer called a vptr, and it does not require every implementation to use the same vtable layout.

Therefore “one virtual function always adds one pointer's worth of size to the object” is only a rule of thumb for simple cases under a particular ABI. Alignment, reuse of a primary-base layout, multiple inheritance, and virtual inheritance all change layout. A compiler may even eliminate a virtual call or the object itself, provided observable behavior is unchanged.

How a common ABI supports multiple inheritance

In the Itanium C++ ABI, for example, a vtable may hold not only function entries but also offsets to the complete object, type information, and virtual-base location data. Different base subobjects in a multiple-inheritance hierarchy may need different vtable views. A call entry may also go through a thunk that adjusts this before entering the derived implementation.

Those details explain why starting from different base pointers can still correctly reach the derived object. They are not a public interface for ordinary programs to manipulate by hand, and you must not assume that every vtable slot is an address that can be called as an ordinary function pointer. Study layout with the compiler's layout dump, a debugger, and the ABI document for the target platform.

Verify behavior; do not probe unguaranteed bytes

The example calls the implementation of one complete object through two base interfaces, and it uses a well-formed dynamic_cast for a cross-cast, verifying standard-level behavior. It does not read an integer at the start of the object, and it does not cast the object address to a multi-level pointer and dereference it. It therefore does not depend on layout, aliasing rules, or a coincidental result from one compiler.

When the compiler can prove the dynamic type, it may devirtualize and inline the call. final can sometimes help provide that proof, but it does not guarantee that any particular assembly instruction disappears. Performance judgments should rest on a real optimized build and on actual data distribution, not on charging a fixed cost merely because the source text contains virtual.

Pitfalls

  • reinterpret_cast of an object pointer to a vtable pointer followed by a read is not a standard-blessed general reflection mechanism. An experiment that appears to work does not prove portability.
  • One vtable per class and one vptr per object are not universally true layout rules, especially with multiple and virtual inheritance.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>

struct Reader {
    virtual int read() const = 0;
    virtual ~Reader() = default;
};
struct Writer {
    virtual void write(int value) = 0;
    virtual ~Writer() = default;
};
class Cell final : public Reader, public Writer {
    int value_ = 3;
public:
    int read() const override { return value_; }
    void write(int value) override { value_ = value; }
};

int main() {
    Cell cell;
    Reader* reader = &cell;
    Writer* writer = &cell;
    assert(reader->read() == 3);
    writer->write(8);
    assert(reader->read() == 8);
    assert(dynamic_cast<Writer*>(reader) == writer);
    assert(dynamic_cast<void*>(reader) == static_cast<void*>(&cell));
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-vtable.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Can you verify that there are two interfaces here by asserting sizeof(Cell) == 2 * sizeof(void*) + sizeof(int)?

Show a reference answer

No. Interface behavior is guaranteed by the language rules; sizeof also depends on alignment, padding, and a concrete ABI layout. The given equality may even fail on a common platform because of trailing padding. Keep the behavioral assertions that read and write through both interfaces and that perform the cross-cast. If you study size, record a measurement on the target platform and make clear that it is not a cross-platform contract.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog