Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 17

Vtable and vptr in C++ Object Structure

Recommended resources

Hello! Welcome back to our journey into the internals of C++.

In our last lesson, we established a critical piece of the C++ object model: the memory layout for single inheritance. We saw that a derived class object contains a base class subobject and that when the base class is polymorphic (i.e., has at least one virtual function), the compiler inserts a hidden virtual pointer (vptr) at the very beginning of the object's memory.

Today, we will follow that pointer. Our goal is to analyze the vtable (virtual method table) structure and understand the vptr's placement within an object. We'll move from knowing that the vptr exists to understanding precisely what it points to and how that structure—the vtable—is organized. This is the heart of C++'s runtime polymorphism mechanism.

1. The vptr and vtable: A Fundamental Partnership

The relationship between the vptr and the vtable is the key to dynamic dispatch. Let's be very clear on the distinction:

  • The vptr is a pointer that exists within each object of a polymorphic class. If you create 100 Dog objects, you create 100 vptrs.
  • The vtable is a static lookup table that exists once per class. It's generated by the compiler and is shared by all objects of that class. All 100 of your Dog objects will have vptrs that point to the same single Dog vtable.

This vtable is typically stored in a read-only data segment of the compiled program, like the .rodata section on Linux, which makes sense since it shouldn't be modified at runtime.

The diagram below illustrates this relationship. An object on the stack or heap contains a vptr which points to the vtable in the read-only data section. The vtable itself is an array of function pointers, and each of these pointers points to executable code in the code section (or .text section).

C++ vptr and vtable Mechanism
This image shows the indirection: the object's `vptr` provides the address of the correct `vtable`, and the `vtable` provides the addresses of the virtual functions.

While the C++ standard doesn't mandate the existence of vptrs and vtables, it is the de-facto implementation used by virtually all major compilers, including GCC and Clang, as specified by the Itanium C++ ABI.

To see a clear explanation of this architecture, watch the following clip.

C++ Inheritance Explained: V-Tables, Virtual Destructors & Edge Cases

The video 'C++ Inheritance Explained' from the DeepDiveDev channel provides an excellent visualization of this concept.

Watch the segment from 05:44 to 07:02. It crisply explains the distinction between the per-object vptr and the per-class vtable, and how the compiler uses this structure to find the right function at runtime.


2. The Anatomy of a vtable in Single Inheritance

At its core, a vtable is an array of function pointers. When a derived class inherits from a base class, it doesn't just inherit members; it also inherits the layout of the vtable. The compiler constructs the vtable for a derived class with these rules:

  1. Copy: It starts with a copy of the base class's vtable.
  2. Overwrite: If the derived class overrides a virtual function, the compiler replaces the corresponding pointer in the vtable with the address of the derived class's implementation.
  3. Append: If the derived class introduces new virtual functions, their addresses are appended to the end of the vtable.

Let's consider this simple hierarchy:

class Shape {
public:
    virtual void draw() { /* draws a generic shape */ }
    virtual float area() { return 0.0; }
    // virtual ~Shape() {} // We'll add this later
};

class Circle : public Shape {
public:
    void draw() override { /* draws a circle */ } // Overwrites
    // area() is inherited
    virtual void roll() { /* rolls the circle */ } // Appends
};

The vtables would look like this:

  • vtable for Shape:

    • Index 0: &Shape::draw
    • Index 1: &Shape::area
  • vtable for Circle:

    • Index 0: &Circle::draw (Overwritten pointer)
    • Index 1: &Shape::area (Inherited pointer)
    • Index 2: &Circle::roll (Appended pointer)

This layout is crucial. Because the Circle vtable maintains the same layout as the Shape vtable for its initial entries, code holding a Shape* can call draw() or area() using the same index (0 or 1) regardless of whether the pointer points to a Shape or a Circle. This is the essence of how polymorphism is efficiently implemented.


3. Deconstructing the Magic: Building Polymorphism in C

Given your strong background in C, the best way to truly understand this mechanism is to build it yourself. The C++ compiler is essentially automating a design pattern that you can replicate manually using structs, function pointers, and careful memory layout.

The following video does exactly this. It's a powerful demonstration that strips away the C++ "magic" and reveals the mechanical, predictable system underneath.

C++ Inheritance Explained: V-Tables, Virtual Destructors & Edge Cases

The 'C++ Inheritance Explained' video continues with a brilliant segment where it implements polymorphism using only C constructs. This will connect directly with your existing knowledge.

Watch from 07:02 to 10:49. Pay close attention to how they simulate inheritance by embedding structs and how the vptr is manually set in their 'constructor' functions. This perfectly mirrors what the C++ compiler does for you implicitly.


4. Exploring the Full vtable: Hidden Entries and ABI Details

The vtable holds more than just pointers to the virtual functions you declare. The ABI specifies several other important entries that are used for the C++ runtime system. For someone who wants to know how things work from first principles, these details are fascinating.

The following article is a fantastic hands-on guide that uses modern C++ to inspect the memory of an object, find its vptr, and explore the full contents of its vtable.

Visualizing the C++ Object Memory Layout Part 1: Single ...

The article 'Visualizing the C++ Object Memory Layout Part 1: Single Inheritance' by Sofia Belen is an exceptional deep dive. It will guide you through programmatically exploring the vptr and vtable.

Please read the following sections: 'Invoking Functions Directly via Vptr': This shows you the exact casting and pointer arithmetic needed to access the vtable and manually call a virtual function. 'Difference Between Deleting and Complete Object Destructors': This reveals a key implementation detail about virtual destructors—that they often have two entries in the vtable for different use cases. 'Vtable Entries [-1] and [-2]': This uncovers even more hidden entries, including the pointer to Run-Time Type Information (RTTI) and the offset-to-top, which is a hint towards how multiple inheritance is handled.

After working through that article, you'll have a complete picture of the vtable structure under the Itanium C++ ABI, including its "hidden" administrative entries. Specifically, for a derived class D inheriting from a base B:

  • vtable[-2]: offset_to_top (0 in single inheritance)
  • vtable[-1]: typeinfo pointer for D (used by typeid and dynamic_cast)
  • vtable[0]: Complete object destructor for D
  • vtable[1]: Deleting destructor for D
  • vtable[2]...: Pointers to D's virtual functions, following the copy-overwrite-append rule.
Test your understanding!

Consider the following class hierarchy on a 64-bit system:

class Device {
public:
    virtual ~Device() {}
    virtual bool ping() = 0; // Pure virtual
    virtual void reset() { /* default reset */ }
};

class Router : public Device {
public:
    ~Router() override {}
    bool ping() override { return true; }
    // reset() is inherited
    virtual void configure_firewall() { /* ... */ }
};

Describe the likely layout of the vtable for the Router class, including any hidden entries discussed in the lesson. List the function pointers in the order they would appear.

Show answer

The vtable for the Router class would likely be laid out as follows:

  • vtable[-2]: offset_to_top (Value would be 0).
  • vtable[-1]: typeinfo pointer for the Router class.
  • vtable[0]: Pointer to the complete object destructor for Router.
  • vtable[1]: Pointer to the deleting destructor for Router.
  • vtable[2]: Pointer to Router::ping. This entry overwrites the base class slot for ping.
  • vtable[3]: Pointer to Device::reset. This entry is inherited from the base class vtable.
  • vtable[4]: Pointer to Router::configure_firewall. This is a new virtual function appended to the end.

Note that even though Device::ping is pure virtual and has no implementation, it still occupies a slot in the Device vtable (often filled with a null pointer or a pointer to a generic error function) to maintain the layout. The Router class must provide an implementation to be concrete.


Conclusion

Today we demystified the core of C++ polymorphism. We moved from the per-object vptr to the shared, per-class vtable and dissected its structure.

Key Takeaways:

  • The vptr in each object points to a single, shared vtable for its class, which resides in read-only memory.
  • A vtable is primarily an array of function pointers, enabling runtime lookup of virtual functions.
  • In single inheritance, a derived class's vtable preserves the layout of its base's vtable, overwriting pointers for overridden functions and appending pointers for new ones.
  • The vtable contains more than just function pointers; it includes hidden entries for RTTI, destructors, and other ABI-specific information crucial for the C++ runtime.

Now that we understand the data structures involved (vptr and vtable), our next lesson will put them to work. We will trace the sequence of operations in a virtual function call, analyzing the assembly instructions and performance overhead compared to a non-virtual call.