Virtual functions and polymorphism

Introduction

Polymorphism ("many forms") lets one piece of code work with objects of many classes. In C++ it comes from virtual functions called through base-class pointers or references.

Inheritance on its own reuses code. Polymorphism is what makes it powerful: a list of shapes, animals or employees can be handled by one loop, while each object behaves in its own way.

How it works

Virtual functions

#include <iostream>
using namespace std;

class Shape {
public:
    virtual double area() const {
        return 0;
    }
    virtual ~Shape() {}
};

class Circle : public Shape {
    double r;
public:
    Circle(double r) : r(r) {}
    double area() const override {
        return 3.14159 * r * r;
    }
};

class Square : public Shape {
    double side;
public:
    Square(double s) : side(s) {}
    double area() const override {
        return side * side;
    }
};

int main() {
    Shape* shapes[3] = {new Circle(1), new Square(2), new Circle(0.5)};
    double total = 0;
    for (int i = 0; i < 3; i++) {
        Shape* s = shapes[i];
        total += s->area();
    }
    cout << "total area " << total << endl;
    for (int i = 0; i < 3; i++) delete shapes[i];
    return 0;
}
Open in compiler

shapes is an array of Shape *, but each pointer points to a Circle or a Square. Because area() is virtual, s->area() runs the version for the object's real class, decided while the program runs. Watch the frames: the same line calls Circle::area, then Square::area. Without virtual, every call would run Shape::area, chosen from the pointer's type alone.

  • override in the derived class asks the compiler to check that you really override a virtual function (and did not misspell it, or change its parameters).
  • Each object of a class with virtual functions carries a hidden pointer, the vptr, to its class's table of functions. That is how the call finds the right version. StepVioz keeps it hidden in the memory view.

Syntax

class Base {
public:
    virtual void act() const;          // can be overridden
    virtual ~Base() {}                 // virtual destructor
};
class Derived : public Base {
public:
    void act() const override;         // the compiler checks it
};

Example

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() const {
        cout << "..." << endl;
    }
    virtual ~Animal() {
        cout << "~Animal" << endl;
    }
};

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

int main() {
    Animal* p = new Dog();
    p->speak();
    delete p;
    return 0;
}
Open in compiler

delete p through an Animal * must run the derived destructor too, or the derived part's resources leak. That only happens if the base destructor is virtual. Here ~Dog and then ~Animal run, in that order. Rule: a class with any virtual function needs a virtual destructor.

Line by line

  1. Animal declares speak and its destructor as virtual.
  2. Dog overrides speak and marks it with override.
  3. Animal* p = new Dog(); stores a Dog behind an Animal pointer.
  4. p->speak() prints Woof, because the call is decided by the real object, a Dog.
  5. delete p runs ~Dog and then ~Animal, because the base destructor is virtual.

Expected result

Woof
~Dog
~Animal

In the real world

Plug-in systems, device drivers and GUI event handlers are built on virtual functions: the framework calls a function through a base pointer, and your derived class supplies what happens. Unit-testing frameworks use the same idea to swap in test doubles.

Common mistakes

  • Forgetting virtual in the base class: the base version always runs.
  • Overriding with different parameters (area(int) instead of area()): it is a different function. override catches this.
  • Copying a derived object into a base object (Shape s = circle;) cuts off the derived part (slicing). Polymorphism needs pointers or references.

Best practices

  • Mark every overriding function with override.
  • Give every base class with a virtual function a virtual destructor.
  • Handle polymorphic objects through pointers or references.

Summary

Virtual functions let a base pointer or reference call the version that belongs to the object's real class. override catches mistakes, slicing is avoided by never copying into a base object, and a polymorphic base class needs a virtual destructor.

Quick check

1. Shape *s = new Circle(1); area() is virtual. Which runs for s->area()?
2. What does override do?
3. Why should a base class with virtual functions have a virtual destructor?

Key takeaways

  • virtual makes the object's real class choose the function.
  • Polymorphism works through pointers and references, never through copies.
  • A class with virtual functions needs a virtual destructor.

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.