Skip to content

Polymorphism — Virtual Functions, vtable, override, and final

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Polymorphism. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ polymorphism allows derived class functions to be called through base class pointers or references using virtual functions and a virtual table (vtable) for dynamic dispatch.

What You'll Learn

You will declare virtual functions for polymorphic behavior, understand the vtable mechanism and its overhead, use override to catch signature mismatches at compile time, use final to prevent overriding, call base class implementations from overridden functions, and understand when virtual dispatch is resolved at compile time versus runtime.

Why It Matters

Polymorphism is the third pillar of OOP (Encapsulation, inheritance, polymorphism). It lets you write code that operates on base class interfaces while executing derived class implementations. This is how C++ supports the Open-Closed Principle: code is open for extension (new derived classes) but closed for modification (existing code uses base class pointers/references).

Learning Path

graph LR
    A["15: Inheritance"] --> B["16: Polymorphism"]
    B --> C["17: Abstract Classes"]
    C --> D["18: Multiple Inheritance"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Virtual Functions

#include <iostream>
#include <vector>
#include <memory>

class Shape {
public:
    virtual double area() const {
        return 0.0;
    }
    
    virtual void draw() const {
        std::cout << "Drawing a shape\n";
    }
    
    virtual ~Shape() = default;
};

class Circle : public Shape {
private:
    double radius_;
    
public:
    Circle(double r) : radius_(r) {}
    
    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
    
    void draw() const override {
        std::cout << "Drawing a circle (radius=" << radius_ << ")\n";
    }
};

class Rectangle : public Shape {
private:
    double width_, height_;
    
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    
    double area() const override {
        return width_ * height_;
    }
    
    void draw() const override {
        std::cout << "Drawing a rectangle (" << width_ << "x" << height_ << ")\n";
    }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
    
    for (const auto& s : shapes) {
        s->draw();
        std::cout << "Area: " << s->area() << "\n";
    }
}

Expected output:

Drawing a circle (radius=5)
Area: 78.5397
Drawing a rectangle (3x4)
Area: 12

The vtable Mechanism

When a class has at least one virtual function, the compiler generates a virtual table (vtable) for that class. Each object of that class contains a hidden pointer (vptr) pointing to the class's vtable.

Object of Circle:
+-----------+
| vptr      | --> Circle's vtable:
| radius_   |     +-----------------+
+-----------+     | type_info       |
                   | ~Circle()      |
                   | area()         | --> Circle::area()
                   | draw()         | --> Circle::draw()
                   +-----------------+

When you call shape->draw(), the compiler:

  1. Follows the vptr to the vtable
  2. Looks up draw() at the correct offset in the vtable
  3. Calls the function pointer stored there

This indirection has a small runtime cost but enables dynamic dispatch. The vtable itself is generated once per class and shared among all instances.

The override Specifier

class Base {
public:
    virtual void foo(int x) {}
    virtual void bar() const {}
};

class Derived : public Base {
public:
    // Without override, this is a NEW function (hides Base::foo)
    void foo(int x) override {  // OK: matches Base::foo
    }
    
    // void bar() override { }  // Error: Base::bar is const, this is not
};

override tells the compiler to verify that the function actually overrides a base class virtual function. If the signature does not match, the compiler errors. Always mark overriding functions with override.

The final Specifier

class Base {
public:
    virtual void f() {}
};

class Derived final : public Base {
public:
    void f() override final {
        // Cannot be overridden further
    }
};

// class GrandChild : public Derived { };  // Error: Derived is final

final on a class prevents further derivation. final on a virtual function prevents further overriding. Use final to seal class hierarchies and enable compiler optimizations (devirtualization).

Calling Base Class Implementations

#include <iostream>

class Base {
public:
    virtual void log() const {
        std::cout << "[Base] ";
    }
};

class Derived : public Base {
public:
    void log() const override {
        Base::log();  // explicit call to base version
        std::cout << "[Derived] ";
    }
};

int main() {
    Derived d;
    d.log();     // [Base] [Derived]
    
    Base& ref = d;
    ref.Base::log();  // [Base] (bypasses virtual dispatch)
}

Calling Base::log() explicitly bypasses virtual dispatch. This is useful when a derived class wants to extend (not replace) the base behavior.

Virtual Destructors

class Base {
public:
    virtual ~Base() = default;  // ALWAYS virtual in polymorphic base
};

class Derived : public Base {
    int* data_ = new int[100];
public:
    ~Derived() override {
        delete[] data_;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // without virtual destructor: undefined behavior, Derived leaks
}

When Virtual Functions Are Not Used

void printAreaByValue(Shape s) {  // SLICING: no polymorphism
    std::cout << s.area() << "\n";  // always calls Shape::area()
}

void printAreaByRef(const Shape& s) {  // polymorphism works
    std::cout << s.area() << "\n";
}

Polymorphism only works with references and pointers. Passing by value slices the object, removing the vtable pointer.

Common Mistakes

Mistake 1: Forgetting virtual in Base Class

class Base {
public:
    void f() {}  // not virtual
};
class Derived : public Base {
public:
    void f() {}  // hides, not overrides
};
Base* p = new Derived();
p->f();  // calls Base::f, not Derived::f

Mistake 2: Missing override — Signature Mismatch

class Derived : public Base {
public:
    void f(int x) override;  // Error if Base::f takes double
};

Use override to catch these mismatches.

Mistake 3: Calling Virtual Functions in Constructor/Destructor

During construction and destruction, the dynamic type is the class being constructed/destroyed, not the most derived type. Virtual function calls resolve to the current class's version.

Mistake 4: Non-Virtual Destructor

If you delete a derived object through a base pointer and the base destructor is not virtual, the derived destructor never runs, and resources leak.

Mistake 5: Assuming Virtual Calls are Always Dynamic

The compiler can devirtualize calls when the dynamic type is known at compile time (e.g., calling a virtual function on a stack-allocated object).

Practice Questions

  1. What is the vtable and how does it enable polymorphism?
  2. What does the override keyword do? Why should you always use it?
  3. Why do virtual destructors matter in polymorphic hierarchies?
  4. How can you call the base class version of a virtual function from derived code?
  5. What is the difference between early binding and late binding?

Challenge

Create a plugin-style architecture: define an Effect base class with a virtual apply function. Implement InvertEffect, BlurEffect, and GrayscaleEffect derived classes. Store pointers in a vector and call apply polymorphically.

FAQ

What is the performance cost of virtual functions?

Each virtual call involves an extra pointer dereference (vptr -> vtable -> function pointer). The cost is small (a few CPU cycles) but prevents inlining. For performance-critical inner loops, consider non-virtual alternatives.

Can virtual functions be inlined?

Sometimes. If the compiler can determine the dynamic type at compile time (e.g., stack-allocated object, or after devirtualization), it can inline the call.

What is the difference between virtual and non-virtual inheritance?

Virtual inheritance affects base class subobject layout in derived classes. Virtual functions affect runtime dispatch of member functions. They are orthogonal concepts.

How does C++ handle virtual function calls during construction?

During the base class constructor, the vptr points to the base class vtable. Virtual calls resolve to the base version, not the derived version. This prevents calling functions on uninitialized derived parts.

Can I have a virtual function with a default implementation?

Yes. A virtual function can have a body in the base class. Derived classes may override it or use the default.

What is 'RTTI' and how does it relate to polymorphism?

Run-Time Type Information (dynamic_cast, typeid) requires at least one virtual function in the class. RTTI uses the vtable pointer to determine the dynamic type.

Mini Project

Build a polymorphic logging system:

#include <iostream>
#include <vector>
#include <memory>
#include <fstream>

class Logger {
public:
    virtual ~Logger() = default;
    virtual void log(const std::string& message) = 0;
};

class ConsoleLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << "[Console] " << message << "\n";
    }
};

class FileLogger : public Logger {
private:
    std::ofstream file_;
public:
    FileLogger(const std::string& path) {
        file_.open(path);
    }
    void log(const std::string& message) override {
        file_ << "[File] " << message << "\n";
        file_.flush();
    }
};

class FilteredLogger : public Logger {
private:
    std::unique_ptr<Logger> wrapped_;
    std::string prefix_;
public:
    FilteredLogger(std::unique_ptr<Logger> wrapped, const std::string& prefix)
        : wrapped_(std::move(wrapped)), prefix_(prefix) {}
    
    void log(const std::string& message) override {
        if (message.find(prefix_) == 0) {
            wrapped_->log(message);
        }
    }
};

int main() {
    std::vector<std::unique_ptr<Logger>> loggers;
    loggers.push_back(std::make_unique<ConsoleLogger>());
    loggers.push_back(std::make_unique<FileLogger>("log.txt"));
    loggers.push_back(std::make_unique<FilteredLogger>(
        std::make_unique<ConsoleLogger>(), "ERROR"));
    
    for (const auto& l : loggers) {
        l->log("INFO: System started");
        l->log("ERROR: Disk full");
    }
}

What's Next

Polymorphism enables runtime flexibility. The next lesson covers abstract classes: pure virtual functions, interface classes, and why virtual destructors remain essential in abstract bases.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro