Intermediate 13 min

Virtual Functions

Master virtual functions to enable runtime polymorphism and dynamic dispatch

Learn how virtual functions enable runtime polymorphism, allowing base class pointers to call the correct derived class methods dynamically.

A Simple Example

#include <iostream>
#include <string>
#include <vector>

class Shape {
protected:
    std::string name;

public:
    Shape(std::string name) : name{name} {
    }

    virtual void draw() const {
        std::cout << "Drawing a generic shape: " << name << "\n";
    }

    virtual double area() const {
        return 0.0;
    }

    virtual ~Shape() {
        std::cout << "Shape destructor: " << name << "\n";
    }
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(std::string name, double r) : Shape{name}, radius{r} {
    }

    void draw() const override {
        std::cout << "Drawing circle: " << name << " with radius " << radius << "\n";
    }

    double area() const override {
        return 3.14159 * radius * radius;
    }

    ~Circle() override {
        std::cout << "Circle destructor: " << name << "\n";
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(std::string name, double w, double h)
        : Shape{name}, width{w}, height{h} {
    }

    void draw() const override {
        std::cout << "Drawing rectangle: " << name << " (" << width << "x" << height << ")" << "\n";
    }

    double area() const override {
        return width * height;
    }

    ~Rectangle() override {
        std::cout << "Rectangle destructor: " << name << "\n";
    }
};

int main() {
    std::vector<Shape*> shapes;

    shapes.push_back(new Circle{"Circle1", 5.0});
    shapes.push_back(new Rectangle{"Rect1", 4.0, 6.0});
    shapes.push_back(new Circle{"Circle2", 3.0});

    std::cout << "Drawing all shapes:" << "\n";
    for (const Shape* shape : shapes) {
        shape->draw();  // Calls correct derived class method!
        std::cout << "Area: " << shape->area() << "\n";
    }

    std::cout << "\nCleaning up:" << "\n";
    for (Shape* shape : shapes) {
        delete shape;  // Calls correct derived class destructor!
    }

    return 0;
}

Breaking It Down

The virtual Keyword

  • What it does: Marks a function for dynamic dispatch - calls resolved at runtime, not compile-time
  • Without virtual: Base class pointer calls base version even if object is derived type
  • With virtual: Calls the correct derived class version based on actual object type
  • Remember: Virtual functions enable true polymorphism in C++

The override Keyword

  • What it does: Explicitly marks a function as overriding a base class virtual function
  • Benefit: Compiler catches typos and signature mismatches (e.g., missing const)
  • Not required but strongly recommended for safety and clarity
  • Remember: Override catches bugs at compile-time instead of runtime

Virtual Destructors

  • Why critical: Without virtual destructor, derived destructors never called when deleting via base pointer
  • Consequence: Memory leaks and resources not released properly
  • Rule: Always make destructors virtual in base classes with virtual functions
  • Remember: Virtual destructor prevents object slicing and ensures proper cleanup

How Virtual Functions Work (vtable)

  • Behind the scenes: Each object with virtual functions contains a hidden vptr (virtual pointer)
  • The vptr points to a vtable (virtual function table) with function addresses
  • Runtime cost: One extra pointer dereference per virtual call
  • Memory cost: One pointer per object, one vtable per class

Why This Matters

  • Virtual functions are the key to true polymorphism in C++.
  • They let you write code that works with base class pointers but automatically calls the right derived class function at runtime.
  • This is how you build extensible systems - add new derived classes without changing existing code.
  • It's the foundation of plugin systems, game engines, UI frameworks, and countless other applications.
  • Understanding virtual functions transforms you from someone who uses classes to someone who designs flexible, maintainable systems.

Critical Insight

Virtual functions work through a hidden pointer (vptr) in each object that points to a virtual function table (vtable) containing function addresses. This is why virtual functions have a tiny runtime cost - an extra pointer dereference. But without virtual, C++ would call the wrong function!

class Base {
public:
    void nonVirtual() { std::cout << "Base nonVirtual\n"; }
    virtual void virt() { std::cout << "Base virtual\n"; }
};

class Derived : public Base {
public:
    void nonVirtual() { std::cout << "Derived nonVirtual\n"; }
    void virt() override { std::cout << "Derived virtual\n"; }
};

Base* ptr = new Derived{};
ptr->nonVirtual();  // Calls Base version - wrong!
ptr->virt();        // Calls Derived version - correct!
// Virtual = runtime type, non-virtual = compile-time type

Best Practices

Always use virtual destructors: If a class has virtual functions, make the destructor virtual to ensure proper cleanup when deleting through base pointers.

Use override keyword: Always mark overriding functions with override to catch signature mismatches at compile-time.

Prefer smart pointers: Use std::unique_ptr<Shape> or std::shared_ptr<Shape> instead of raw pointers to avoid manual memory management.

Mark final if no further derivation: Use final keyword on functions or classes that should not be overridden or inherited from.

Common Mistakes

Forgetting Virtual Destructor: Without a virtual destructor, deleting through a base pointer won't call derived destructors, causing memory leaks.

Not Using override Keyword: Omitting override can hide bugs where you think you're overriding but you're not.

Calling Virtual Functions in Constructors: During construction, the object hasn't become the derived type yet, so virtual calls resolve to the base version.

Slicing Objects: Assigning derived object to base class value (not pointer) slices away derived parts.

Debug Challenge

This Shape hierarchy has a bug - when deleting shapes through base pointers, the derived destructors are never called. Click the highlighted line to fix it:

1 #include <iostream>
2 #include <vector>
3
4 class Shape {
5 public:
6 ~Shape() {
7 std::cout << "~Shape" << "\n";
8 }
9 };
10
11 class Circle : public Shape {
12 public:
13 ~Circle() {
14 std::cout << "~Circle" << "\n";
15 }
16 };

Quick Quiz

  1. What does the virtual keyword enable?
Enables runtime polymorphism through dynamic dispatch
Makes functions faster
Makes functions static
  1. Why must destructors be virtual in base classes?
To ensure derived class destructors are called when deleting through base pointer
It's optional
To make them faster
  1. What does the override keyword do?
Explicitly marks a function as overriding a base class virtual function
Creates a new virtual function
Makes functions non-virtual

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once