Skip to content
Laviyx Learn

Try

C++ course menu

C++ Polymorphism: Virtual Functions, override and Virtual Destructors

Learn runtime polymorphism in C++: virtual functions, calling through base references and pointers, the override and final keywords, why base classes need virtual destructors, object slicing, dynamic_cast, and how the vtable works.

Intermediate16 min readUpdated
On this page

Polymorphism ("many forms") lets one piece of code work with objects of different classes through a common base class, and still run the right behaviour for each one. A function that draws a Shape can draw circles, squares and triangles, without knowing which it has. In C++ this is done with virtual functions.

The problem without virtual#

C++
#include <iostream>

class Animal {
public:
    void speak() const { std::cout << "...\n"; }
};

class Dog : public Animal {
public:
    void speak() const { std::cout << "Woof\n"; }
};

void make_noise(const Animal& a) {
    a.speak();
}

int main() {
    Dog d;
    d.speak();         // Dog::speak
    make_noise(d);     // Animal::speak, not what we wanted
}
Output
Woof
...

Inside make_noise, the compiler only knows it has an Animal&, so it calls Animal::speak. The decision is made at compile time from the declared type.

virtual: decide at run time#

Marking the base method virtual tells C++ to look at the actual object's type when the program runs:

C++
#include <iostream>

class Animal {
public:
    virtual void speak() const { std::cout << "...\n"; }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "Woof\n"; }
};

class Cat : public Animal {
public:
    void speak() const override { std::cout << "Meow\n"; }
};

void make_noise(const Animal& a) {
    a.speak();
}

int main() {
    Dog d;
    Cat c;
    make_noise(d);
    make_noise(c);
}
Output
Woof
Meow

Polymorphism works through a reference or pointer to the base class. make_noise was written once, and it works for every current and future kind of Animal.

override: let the compiler check#

override says "this function is meant to override a virtual function in a base class". If the signature does not match (a typo, a missing const), the compiler reports an error instead of silently creating a new, unrelated function:

C++
class Animal {
public:
    virtual void speak() const {}
};

class Dog : public Animal {
public:
    void speak() override {}   // error: missing const, so it overrides nothing
};

Always write override on overriding functions. (virtual is only needed in the base class.) final prevents further overriding of a function, or further inheritance from a class.

A collection of different objects#

The classic use is a container of base-class pointers. Smart pointers own the objects:

C++
#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual double area() const = 0;
    virtual const char* name() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return 3.14159 * r_ * r_; }
    const char* name() const override { return "Circle"; }

private:
    double r_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    const char* name() const override { return "Rectangle"; }

private:
    double w_;
    double h_;
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(1.0));
    shapes.push_back(std::make_unique<Rectangle>(3, 4));
    shapes.push_back(std::make_unique<Circle>(2.0));

    double total = 0;
    for (const auto& s : shapes) {
        std::cout << s->name() << ": " << s->area() << '\n';
        total += s->area();
    }
    std::cout << "Total: " << total << '\n';
}
Output
Circle: 3.14159
Rectangle: 12
Circle: 12.5664
Total: 27.708

= 0 makes a function pure virtual: Shape provides no implementation, and every concrete shape must. See Abstract Classes.

Virtual destructors#

If you delete a derived object through a base pointer, the base class must have a virtual destructor. Otherwise only the base destructor runs, which is undefined behaviour and usually leaks the derived part:

C++
#include <iostream>
#include <memory>

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

class Derived : public Base {
public:
    ~Derived() override { std::cout << "~Derived\n"; }
};

int main() {
    std::unique_ptr<Base> p = std::make_unique<Derived>();
}   // deletes through a Base pointer
Output
~Derived
~Base

Rule: any class with a virtual function should have a virtual destructor (virtual ~Base() = default; is enough).

Object slicing#

Polymorphism needs references or pointers. Copying a derived object into a base object keeps only the base part, which is called slicing:

C++
#include <iostream>

class Animal {
public:
    virtual void speak() const { std::cout << "...\n"; }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "Woof\n"; }
};

void by_value(Animal a) { a.speak(); }          // copies: slices off Dog
void by_reference(const Animal& a) { a.speak(); }

int main() {
    Dog d;
    by_value(d);
    by_reference(d);
}
Output
...
Woof

Pass polymorphic objects by reference or pointer, and store them in containers as smart pointers, never as base-class values.

dynamic_cast: asking for the real type#

Occasionally you need to know whether a base pointer really points to a particular derived type. dynamic_cast checks at run time and returns nullptr if it does not:

C++
#include <iostream>
#include <memory>
#include <vector>

class Animal {
public:
    virtual ~Animal() = default;
};
class Dog : public Animal {
public:
    void fetch() const { std::cout << "fetching the ball\n"; }
};
class Cat : public Animal {};

int main() {
    std::vector<std::unique_ptr<Animal>> pets;
    pets.push_back(std::make_unique<Cat>());
    pets.push_back(std::make_unique<Dog>());

    for (const auto& pet : pets) {
        if (auto* dog = dynamic_cast<Dog*>(pet.get())) {
            dog->fetch();
        } else {
            std::cout << "not a dog\n";
        }
    }
}
Output
not a dog
fetching the ball

Needing dynamic_cast often is a sign that the behaviour should be a virtual function in the base class instead.

How it works: the vtable#

Each class with virtual functions has a hidden table of function pointers (the vtable), and each object stores a hidden pointer to its class's table. A virtual call follows that pointer to find the right function. The cost is one extra indirection per call and one pointer per object, which is small but is why C++ does not make every function virtual automatically.

Common mistakes#

Practice#

Practice exercise

Create a base class Notification with a pure virtual void send(const std::string& message) const, and two classes EmailNotification and SmsNotification that print Email: ... and SMS: .... Store one of each in a vector of std::unique_ptr<Notification> and send "Your order has shipped" through all of them.

Show solution
C++
#include <iostream>
#include <memory>
#include <string>
#include <vector>

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

class EmailNotification : public Notification {
public:
    void send(const std::string& message) const override {
        std::cout << "Email: " << message << '\n';
    }
};

class SmsNotification : public Notification {
public:
    void send(const std::string& message) const override {
        std::cout << "SMS: " << message << '\n';
    }
};

int main() {
    std::vector<std::unique_ptr<Notification>> channels;
    channels.push_back(std::make_unique<EmailNotification>());
    channels.push_back(std::make_unique<SmsNotification>());

    for (const auto& channel : channels) {
        channel->send("Your order has shipped");
    }
}
Output
Email: Your order has shipped
SMS: Your order has shipped

Adding a push notification later needs only a new class; the sending loop does not change.

Key takeaway#

Finished this lesson?