Skip to main content

C++ Virtual Tables & Inheritance

C++ virtual tables (vtables) explained. Learn virtual dispatch, single/multiple inheritance, RTTI, and object memory layout visually.

An Animal* can point to a Dog, yet ptr->speak() still calls Dog::speak(). The pointer's static type determines which operations are available; the object's dynamic type determines the virtual implementation.

The diagrams follow that call through a typical vptr and vtable implementation. C++ specifies virtual dispatch behavior, while the compiler's application binary interface (ABI) determines the representation. The Itanium C++ ABI describes the table groups used by many GCC and Clang targets; other ABIs can differ.

From object to function call

Inspect how an Animal pointer reaches a Dog implementation. Switch topics to compare object layout, shared tables, slicing and call cost; use the dispatch and slicing controls to follow each transition.

Follow a virtual call

Trace Animal and Dog through object creation, dispatch and slicing. Choose a topic to inspect its diagram and source.

Virtual dispatch in action

Follow the pointer, object, vptr and function entry. Each playback step exposes one part of the lookup.

Step 1 / 6

Read the Animal pointer

Virtual dispatch in action. Read the Animal pointer

The dynamic type is Dog, so the final overrider is Dog::speak(). The diagram shows an indirect call; an optimizer may devirtualize this particular source.

virtual-dispatch.cpp
Dog myDog;Animal* ptr = &myDog;ptr->speak();
Complete runnable example
virtual_dispatch.cpp
#include <iostream>

struct Animal {
    virtual ~Animal() = default;
    virtual void speak() { std::cout << "Some sound\n"; }
};

struct Dog : Animal {
    void speak() override { std::cout << "Woof!\n"; }
};

struct Cat : Animal {
    int age = 5;
    float weight = 4.0f;
    void speak() override { std::cout << "Meow!\n"; }
};

struct Flying { virtual void fly() {} };
struct Swimming { virtual void swim() {} };
struct Duck : Flying, Swimming {
    void fly() override {}
    void swim() override {}
};

int main() {
    Dog myDog;
    Animal* ptr = &myDog;
    ptr->speak();
    Animal myAnimal = myDog;
    myAnimal.speak();
    Animal& ref = myDog;
    ref.speak();
    std::cout << "Cat: " << sizeof(Cat) << " bytes, alignment "
              << alignof(Cat) << '\n';
    std::cout << "Duck: " << sizeof(Duck) << " bytes\n";
}

Compile with a C++17 compiler. The program prints the dispatch results and your compiler's Cat and Duck sizes.

Authored illustrations: addresses and playback timing are illustrative. Layout examples assume 8-byte pointers and alignment, with 4-byte int and float. ABI details vary; the complete source is available below.

From one base to several

Compare single, multiple and diamond inheritance. Change the selected class to inspect inherited function targets, then switch to Memory Layout or RTTI to connect the object structure to pointer adjustment.

Inspect tables and inheritance

Compare single, multiple and virtual inheritance. Follow the selected class into its object layout and virtual function targets.

Step 1 / 3

Selected target: B::~B(). Complete object size: 16 bytes in this model.

A, B and C share a primary base layout. B overrides Bar; C overrides Foo. Inherited functions retain their original target.

B: A, B and C share a primary base layout. B overrides Bar; C overrides Foo. Inherited functions retain their original target.
Read B layout and function targets as text
Offset 0: vptr
8 bytes
Offset 8: A::a
4 bytes
Offset 12: B::b
4 bytes

Virtual targets: B::~B(), B::Bar(), A::Foo().

Complete runnable single inheritance example
single_inheritance.cpp
#include <iostream>
struct A {
    virtual ~A() = default;
    virtual void Bar() {}
    virtual void Foo() {}
    int a = 0;
};
struct B : A {
    void Bar() override {}
    int b = 0;
};
struct C : B {
    void Foo() override {}
    int c = 0;
};
int main() {
    std::cout << sizeof(A) << ' ' << sizeof(B) << ' ' << sizeof(C) << '\n';
}
Authored diagrams for the supplied source, using a 64-bit Itanium-style ABI layout. Sizes and padding vary with ABI and declarations. Function lists omit destructor pairs, construction tables and adjustment thunks; they are not raw vtable dumps.

What are Virtual Tables?

Virtual tables (vtables) are a common implementation of runtime polymorphism. A virtual table contains function pointers and ABI metadata. A class may need a primary table and secondary tables for its base subobjects; complete objects of the same most-derived type share that table data.

Key Components

1. Virtual Table Pointer (vptr)

  • In this ABI model, polymorphic objects contain one or more hidden vptrs
  • Each points to an address point within the appropriate virtual table
  • A primary vptr is commonly at offset zero; secondary base subobjects can have their own
  • The examples assume 8-byte pointers; this is a target assumption, not a C++ requirement

2. Virtual Table (vtable)

  • Compiler-emitted data containing function pointers and ABI metadata
  • Primary and secondary tables can form a virtual table group
  • Inherited entries can point to base implementations; overrides select derived implementations
  • Derived tables are generated with the appropriate targets; an override does not mutate the base class table

3. Virtual Dispatch Process

C++
Base* ptr = new Derived();
ptr->virtualFunc();  // Virtual dispatch

// Behind the scenes:
// 1. Access object's vptr
// 2. Follow vptr to vtable
// 3. Index into vtable for function
// 4. Call function through pointer

Inheritance Types

Single Inheritance

C++
class A {
public:
    virtual ~A() {}
    virtual void foo() {}
    int a;
};

class B : public A {
public:
    virtual void foo() override {}
    int b;
};

// Memory layout:
// B object: [vptr][a][b]
// One vtable pointer

Multiple Inheritance

C++
class Base1 {
public:
    virtual ~Base1() {}
    virtual void func1() {}
};

class Base2 {
public:
    virtual ~Base2() {}
    virtual void func2() {}
};

class Derived : public Base1, public Base2 {
public:
    virtual void func1() override {}
    virtual void func2() override {}
};

// Memory layout:
// Derived: [vptr1][Base1 data][vptr2][Base2 data][Derived data]
// Multiple vtable pointers!

Diamond Problem & Virtual Inheritance

C++
class Animal {
public:
    virtual ~Animal() {}
    int age;
};

class Mammal : virtual public Animal {
public:
    virtual void walk() {}
};

class Bird : virtual public Animal {
public:
    virtual void fly() {}
};

class Bat : public Mammal, public Bird {
public:
    // Single instance of Animal
};

Memory Layout Details

Alignment Rules

  1. Class Alignment: Must satisfy the ABI requirements of its bases and members, including hidden implementation data.
  2. Padding: Keeps subobjects aligned and rounds the complete size to its alignment. Some base tail padding can be reused.
  3. vtable Pointer: A primary vptr commonly occupies offset zero; multiple inheritance can introduce additional vptrs.
  4. Inheritance Order: Base subobjects, shared virtual bases and derived members follow ABI layout rules. Their layout is not a portable concatenation of class sizes.

Size Calculation

C++
class Example {
public:
    virtual ~Example() {}  // +8 bytes (vptr)
    int x;                 // +4 bytes
    char c;                // +1 byte
    // +3 bytes padding
    double d;              // +8 bytes
};  // Typical model: 24 bytes, alignment 8; verify with sizeof/alignof

RTTI (Run-Time Type Information)

type_info Class

C++
const std::type_info& info = typeid(*ptr);
std::cout << info.name();      // Type name
std::cout << info.hash_code(); // Type hash

dynamic_cast

C++
Base* base = new Derived();

// Safe downcasting
Derived* derived = dynamic_cast<Derived*>(base);
if (derived) {
    // Cast successful
}

// Cross-casting (multiple inheritance)
Base2* base2 = dynamic_cast<Base2*>(base1);

Performance Considerations

  • dynamic_cast: Checked downcasts and cross-casts may perform runtime type lookup and pointer adjustment. Failed pointer casts return nullptr; failed reference casts throw std::bad_cast.
  • static_cast: Does not perform the runtime downcast check. It may still adjust a pointer, and an invalid downcast is unsafe.
  • Virtual dispatch: An indirect call can load the vptr and a function pointer. Optimization can devirtualize or inline a known target.
  • Memory overhead: Depends on vptr count, base subobjects, alignment and shared table data. One 8-byte vptr is the simple example, not a universal per-object cost.

Best Practices

1. Virtual Destructor

C++
class Base {
public:
    virtual ~Base() = default;  // Required for ordinary deletion through Base*
};

2. Override Keyword

C++
class Derived : public Base {
public:
    void func() override;  // Explicit override
};

3. Final Classes/Methods

C++
class Derived final : public Base {  // Cannot be inherited
public:
    void func() final;  // Cannot be overridden
};

4. Pure Virtual Functions

C++
class Interface {
public:
    virtual void func() = 0;  // Pure virtual
};

Common Pitfalls

1. Object Slicing

Copying into a base object slices the derived part

In the example below, Base b = d loses the derived part of the object, while Base& ref = d keeps a reference to the original object.

C++
Derived d;
Base b = d;  // Slicing! Loses derived part
Base& ref = d;  // OK - no slicing

2. Calling Virtual Functions in Constructor

C++
class Base {
public:
    Base() {
        virtualFunc();  // Calls Base::virtualFunc, not Derived!
    }
    virtual void virtualFunc();
};

3. Missing Virtual Destructor

C++
Base* ptr = new Derived();
delete ptr;  // Undefined behavior if ~Base() not virtual

Performance Optimization

1. Devirtualization

Compiler optimizations can eliminate virtual calls when type is known:

C++
Derived d;
d.virtualFunc();  // May be devirtualized

2. Final Optimization

C++
class Derived final : public Base {
public:
    void func() final;  // Compiler can optimize
};

3. CRTP (Curiously Recurring Template Pattern)

C++
template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation() { /* ... */ }
};

Modern C++ Features

1. override and final (C++11)

C++
struct Base {
    virtual void foo();
    virtual void bar() final;
};

struct Derived : Base {
    void foo() override;
    // void bar() override;  // Error: final
};

2. = default and = delete (C++11)

C++
class Modern {
public:
    virtual ~Modern() = default;
    Modern(const Modern&) = delete;  // Non-copyable
};

3. Virtual Function Templates (Not Allowed)

C++
class Invalid {
public:
    template<typename T>
    virtual void func();  // Error! Cannot be virtual
};

Conclusion

Understanding vtables and inheritance is crucial for:

  • Writing efficient polymorphic code
  • Debugging memory layouts
  • Optimizing performance
  • Avoiding common pitfalls

Use the object model to reason about dispatch and ownership, and measure the actual workload before changing a design for performance. Neither the illustrated lookup steps nor the object sizes establish a universal latency cost.

For the language rules, see virtual functions in the C++ working draft. For implementation details, see Itanium object layout and runtime type information.

If you found this explanation helpful, consider sharing it with others.

Mastodon