Loading...

Polymorphism in C++: Virtual Functions and Vtable

Learn polymorphism in C++ using virtual functions, vtables, and dynamic dispatch to build flexible object-oriented applications.

Polymorphism lets objects that share the same interface (base type) exhibit different behaviors at runtime. In C++, dynamic polymorphism is achieved via virtual functions, and compilers typically implement this mechanism using a vtable (virtual table) and a per-object vptr (virtual pointer). In this article, we’ll examine how virtual functions work, the vtable/vptr layout, performance implications, and best practices with realistic examples.


1) What Is Dynamic Polymorphism?

Dynamic polymorphism means which function gets called is decided at runtime. This allows invoking derived behavior through a base class reference or pointer.


#include <iostream>
#include <memory>
using namespace std;

struct Book {
    virtual ~Book() = default;           // required for a polymorphic base
    virtual void PrintInfo() const {     // virtual function
        cout << "[Book] Generic info" << endl;
    }
};

struct EBook : Book {
    void PrintInfo() const override {
        cout << "[EBook] Digital edition" << endl;
    }
};

struct PrintedBook : Book {
    void PrintInfo() const override {
        cout << "[PrintedBook] Physical edition" << endl;
    }
};

int main() {
    unique_ptr<Book> a = make_unique<EBook>();
    unique_ptr<Book> b = make_unique<PrintedBook>();

    a->PrintInfo();  // [EBook] ...
    b->PrintInfo();  // [PrintedBook] ...
}

Although a and b appear as the base type (Book), calls are dispatched at runtime to the actual types (EBook/PrintedBook).


2) How Do vtable and vptr Work?

When a class has at least one virtual function, the compiler generates a vtable for that class. Objects typically contain a hidden vptr that points to the class’s vtable.

At call time: object.vptr → vtable → actual function address.


Book (vptr) ──► vtable(Book)       : &Book::PrintInfo
EBook (vptr) ─► vtable(EBook)       : &EBook::PrintInfo
Printed (vptr)► vtable(PrintedBook) : &PrintedBook::PrintInfo

This indirection adds a small call overhead; in most applications it’s negligible.


3) override, final, and a Virtual Destructor

The override keyword verifies at compile time that you’re truly overriding a virtual function from the base. final marks a method or class as not overridable. In polymorphic bases a virtual destructor is essential; otherwise deleting via a base pointer leads to undefined behavior.


struct Book {
    virtual ~Book() = default;
    virtual double Price() const { return 0.0; }
};

struct EBook final : Book {
    double Price() const override { return 99.0; }
};

// struct SpecialEBook : EBook {}; // ERROR: EBook is final

4) Difference from Static Polymorphism (Overload/Template)

Overloads (same name, different signature) and templates are resolved at compile time and don’t use a vtable. Dynamic polymorphism is resolved at runtime via virtual dispatch. Choose based on your needs:


5) Designing a Polymorphic Interface (Book Example)

Abstract shared behavior in the base class and customize in derived classes. Here’s a price example for digital vs physical:


#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Book {
protected:
    string title, author;
public:
    Book(string t, string a) : title(t), author(a) {}
    virtual ~Book() = default;

    virtual double FinalPrice() const = 0;   // pure virtual: abstract contract
    virtual void PrintInfo() const {
        cout << title << " - " << author
             << " | " << FinalPrice() << " TL" << endl;
    }
};

class EBook : public Book {
    double base, vat; // digital price and VAT
public:
    EBook(string t, string a, double b, double v)
        : Book(t,a), base(b), vat(v) {}

    double FinalPrice() const override {
        return base*(1.0+vat);
    }
};

class PrintedBook : public Book {
    double base, ship; int pages; bool hard;
public:
    PrintedBook(string t, string a, double b, double s, int p, bool h)
        : Book(t,a), base(b), ship(s), pages(p), hard(h) {}

    double FinalPrice() const override {
        double cost = base + ship + 0.04*pages;
        if (hard) cost *= 1.1;
        return cost;
    }
};

int main() {
    unique_ptr<Book> b1 = make_unique<EBook>("Modern C++", "A. Dev", 100, 0.10);
    unique_ptr<Book> b2 = make_unique<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, true);

    b1->PrintInfo(); // EBook behavior
    b2->PrintInfo(); // PrintedBook behavior
}

6) Object Slicing and Using References/Pointers

If you copy a derived object by value into a base type, the derived-specific part gets sliced off. For polymorphism, always use a base reference or base pointer.


PrintedBook pb("X","Y",100,20,200,false);
Book base = pb;   // SLICING: only the Book subobject is copied
base.PrintInfo(); // Book behavior (no derived specifics)

7) dynamic_cast and Type Safety

Use dynamic_cast for safe downcasts from a base type to a derived (requires RTTI). On failure it returns nullptr (for pointers).


void PrintIfPrinted(const Book* b) {
    if (auto pb = dynamic_cast<const PrintedBook*>(b)) {
        cout << "Printed price: " << pb->FinalPrice() << endl;
    } else {
        cout << "Not a PrintedBook." << endl;
    }
}

8) Performance Notes


9) final Class/Method, override Pitfalls, and Best Practices


10) Visualizing the vtable Effect (Step by Step)

  1. The class contains at least one virtual function → the compiler emits a vtable for it.
  2. When an object is constructed, its vptr points to the class’s vtable.
  3. On a call, the compiler fetches the target via vptr → vtable instead of a static address.
  4. An override replaces the corresponding entry in the derived class’s vtable.

11) Common Mistakes


12) Complete Example: A Polymorphic Collection


#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Book {
public:
    virtual ~Book() = default;
    virtual double FinalPrice() const = 0;
    virtual void Print() const = 0;
};

class EBook : public Book {
    double base, vat;
public:
    EBook(double b, double v) : base(b), vat(v) {}
    double FinalPrice() const override { return base*(1+vat); }
    void Print() const override {
        cout << "[EBook] " << FinalPrice() << " TL" << endl;
    }
};

class PrintedBook : public Book {
    double base, ship; int pages;
public:
    PrintedBook(double b, double s, int p) : base(b), ship(s), pages(p) {}
    double FinalPrice() const override { return base + ship + 0.05*pages; }
    void Print() const override {
        cout << "[Printed] " << FinalPrice() << " TL" << endl;
    }
};

int main() {
    vector<unique_ptr<Book>> items;
    items.push_back(make_unique<EBook>(100, 0.1));
    items.push_back(make_unique<PrintedBook>(120, 20, 400));

    for (const auto& it : items) it->Print(); // dynamic polymorphism
}

13) TL;DR

  • Dynamic polymorphism in C++ is powered by virtual functions.
  • Compilers implement virtual dispatch with a vtable/vptr.
  • A virtual destructor is mandatory for polymorphic bases.
  • override enforces signature correctness; final prevents further overrides.
  • Use references/pointers (beware of slicing) for polymorphism instead of copying by value.
  • Overhead is usually small; for hot paths consider alternatives (CRTP/overloads).
  • Examples compile and run on Visual Studio 2022 and GCC 11+.

Related Articles