Fundamentals 26 min read

Mastering C++ Virtual Tables: Demystify Polymorphism and Avoid Pitfalls

This article explains how C++ implements polymorphism through virtual tables and vptrs, walks through single‑ and multiple‑inheritance examples, compares compile‑time and runtime polymorphism, and shows practical scenarios and guidelines for when to use or avoid polymorphic designs.

Deepin Linux
Deepin Linux
Deepin Linux
Mastering C++ Virtual Tables: Demystify Polymorphism and Avoid Pitfalls

Virtual Table (vtable) Generation

When a class declares virtual functions, the compiler creates a vtable containing pointers to those functions in the order of declaration. Pure virtual functions have entries that point to a runtime handler indicating the function is not implemented.

class Base {
    virtual void func1() { std::cout << "Base::func1" << std::endl; }
    virtual void func2() { std::cout << "Base::func2" << std::endl; }
};

class AbstractBase {
    virtual void pureVirtualFunc() = 0;
    virtual void anotherFunc() { std::cout << "AbstractBase::anotherFunc" << std::endl; }
};

vptr and Object Layout

Each object of a class with virtual functions contains a hidden vptr that points to the class’s vtable. On a 64‑bit system the vptr occupies 8 bytes and is typically placed at the start of the object. During construction the vptr is first set to the base‑class vtable; after the base constructor finishes the derived‑class constructor updates it to the derived vtable. Destruction reverses this sequence.

Single‑Inheritance Example

Consider Animal and Dog where Dog overrides speak:

class Animal {
public:
    virtual void speak() { std::cout << "Animal makes a sound" << std::endl; }
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof!" << std::endl; }
};

Allocate memory for a Dog object (includes space for the vptr).

Execute Animal ’s constructor; the vptr points to Animal ’s vtable.

Execute Dog ’s constructor; the vptr is updated to point to Dog ’s vtable, where the speak entry now refers to Dog::speak.

A base‑class pointer Animal* a = new Dog(); invokes a->speak() by looking up the function address via the vptr, resulting in Dog::speak.

Animal* a = new Dog();
a->speak(); // Calls Dog::speak

Multiple‑Inheritance Example

When a class inherits from multiple bases, each base sub‑object may have its own vptr. The following code shows two bases and a derived class that overrides both virtual functions and adds a new one:

class Base1 {
public:
    virtual void func1() { std::cout << "Base1::func1" << std::endl; }
};

class Base2 {
public:
    virtual void func2() { std::cout << "Base2::func2" << std::endl; }
};

class Derived : public Base1, public Base2 {
public:
    void func1() override { std::cout << "Derived::func1" << std::endl; }
    void func2() override { std::cout << "Derived::func2" << std::endl; }
    virtual void func3() { std::cout << "Derived::func3" << std::endl; }
};

Base1* b1 = new Derived();
b1->func1();                     // Calls Derived::func1
Base2* b2 = static_cast<Base2*>(b1);
b2->func2();                     // Calls Derived::func2

Derived objects contain two vptrs: the first points to the Base1 part of the vtable, the second to the Base2 part. Overridden functions replace the base entries; new virtual functions are appended to the first vtable. Virtual inheritance would introduce an additional vbtable/vbptr to resolve the diamond problem.

C++ Polymorphism

Polymorphism allows a single interface to invoke different implementations based on the actual object type.

Compile‑time (static) polymorphism

Implemented via function overloading and templates.

void print(int n) { std::cout << "Printing int: " << n << std::endl; }
void print(double d) { std::cout << "Printing double: " << d << std::endl; }

template<typename T>
void print(T value) { std::cout << "Printing value: " << value << std::endl; }

Runtime (dynamic) polymorphism

Implemented with virtual functions and base‑class pointers or references. The call is resolved at execution time via the vtable.

Three necessary conditions for runtime polymorphism

Inheritance relationship : a base class and at least one derived class.

Virtual function override : the base declares a function virtual, and the derived class provides an overriding definition with the same signature.

Base pointer or reference to a derived object : only calls made through such a pointer/reference trigger dynamic dispatch.

Counter‑examples illustrate each missing condition:

// No inheritance
class A { public: void func() { std::cout << "A::func" << std::endl; } };
class B { public: void func() { std::cout << "B::func" << std::endl; } };
A a; B b; A* p = &a; p->func(); // Calls A::func only
// No virtual override
class Base { public: void func() { std::cout << "Base::func" << std::endl; } };
class Derived : public Base { public: void func() { std::cout << "Derived::func" << std::endl; } };
Base* p = new Derived();
p->func(); // Calls Base::func because func is not virtual
// Direct derived call
Dog d; d.speak(); // Calls Dog::speak directly, no polymorphism

Typical runtime‑polymorphism scenarios

Graphics drawing system : a base Shape declares virtual void draw() const; derived classes Circle, Rectangle, Triangle override it. A function drawShapes(const Shape* s) can render any shape without knowing its concrete type.

Game character behavior : a base Character defines virtual void attack() const; subclasses Warrior, Mage, Assassin provide distinct implementations. The engine calls performAttack(const Character* c) uniformly.

Plugin architecture : an abstract Plugin class declares a pure virtual execute(). Concrete plugins such as BlurPlugin and FormatConverterPlugin implement the method. A PluginManager stores std::unique_ptr<Plugin> objects and runs them.

Interface‑based sorting : an abstract Comparator defines virtual bool compare(const void* a, const void* b) const = 0. Specific comparators for integers and strings implement the method. A generic sort function operates on raw memory using the comparator, demonstrating runtime polymorphism in a type‑agnostic algorithm.

Dependency inversion : a PaymentInterface declares virtual bool processPayment(double) = 0. Concrete classes AlipayPayment and WechatPayment implement the interface. An OrderProcessor depends only on PaymentInterface, allowing the payment method to be swapped without changing order‑processing code.

When to avoid polymorphism

Performance‑critical paths : virtual calls add indirection and cache pressure. In real‑time rendering, high‑frequency math kernels, or cryptographic loops, the overhead can degrade frame rates or throughput. Templates or function overloading eliminate this cost.

Simple, stable hierarchies : if a hierarchy is fixed and small (e.g., only Circle and Rectangle derive from Shape), direct concrete usage avoids the memory and indirection overhead of vtables.

Deterministic initialization or resource management : during system startup or low‑level resource release, predictable call order is essential. Dynamic binding can introduce unexpected overrides that lead to initialization failures or memory‑leak bugs. Static calls guarantee the intended behavior.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

C++polymorphisminheritancevirtual tableruntime dispatchvptr
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.