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.
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#
#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
}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:
#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);
}Woof
MeowPolymorphism 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:
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:
#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';
}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:
#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~Derived
~BaseRule: 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:
#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);
}...
WoofPass 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:
#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";
}
}
}not a dog
fetching the ballNeeding 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
#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");
}
}Email: Your order has shipped
SMS: Your order has shippedAdding a push notification later needs only a new class; the sending loop does not change.
Key takeaway#
Finished this lesson?