Virtual Functions and Polymorphism
Polymorphism lets code work with objects through a base-class reference or pointer while each object behaves according to its real type. Mark a function virtual, override it in derived classes, and a call on a Shape& runs the Circle or Square version as appropriate.
virtual and override
#include <iostream> #include <memory> #include <string> #include <vector> class Animal { public: virtual ~Animal() = default; virtual std::string sound() const { return "..."; } void speak() const { std::cout << name() << " says " << sound() << '\n'; } virtual std::string name() const { return "animal"; } }; class Dog : public Animal { public: std::string sound() const override { return "woof"; } std::string name() const override { return "dog"; } }; class Cat : public Animal { public: std::string sound() const override { return "meow"; } std::string name() const override { return "cat"; } }; int main() { std::vector<std::unique_ptr<Animal>> zoo; zoo.push_back(std::make_unique<Dog>()); zoo.push_back(std::make_unique<Cat>()); zoo.push_back(std::make_unique<Animal>()); for (const auto& a : zoo) a->speak(); }
dog says woof cat says meow animal says ...
speak is written once in Animal, yet each object makes its own sound: the virtual calls are resolved at run time, based on the actual object (dynamic dispatch). Polymorphism only works through references and pointers — copying a Dog into an Animal variable would slice off the dog part.
Always write override
override asks the compiler to check that you really are overriding a base-class virtual function. Without it, a small mismatch — a missing const, a different parameter type — silently creates a new, unrelated function:
#include <string> struct Animal { virtual ~Animal() = default; virtual std::string sound() const { return "..."; } }; struct Bird : Animal { std::string sound() override { return "tweet"; } // missing const }; int main() {}
main.cpp:9:25: error: non-virtual member function marked 'override' hides virtual member function
Virtual destructors
If objects may be deleted through a base-class pointer — including via std::unique_ptr<Base> — the base class must have a virtual destructor. Otherwise only the base part is destroyed, which is undefined behaviour:
#include <iostream> #include <memory> struct Resource { virtual ~Resource() { std::cout << "~Resource\n"; } }; struct FileResource : Resource { ~FileResource() override { std::cout << "~FileResource (file closed)\n"; } }; int main() { std::unique_ptr<Resource> r = std::make_unique<FileResource>(); }
~FileResource (file closed) ~Resource
Rule: a class with any virtual function should have a public virtual destructor (often virtual ~Base() = default;).
Polymorphism through references
Functions taking a base-class reference work with every derived class — including ones written later:
#include <iostream> class Shape { public: virtual ~Shape() = default; virtual double area() const = 0; virtual const char* name() const = 0; }; 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_, h_; }; class Circle : public Shape { public: explicit Circle(double r) : r_(r) {} double area() const override { return 3.14159265 * r_ * r_; } const char* name() const override { return "circle"; } private: double r_; }; void describe(const Shape& s) { std::cout << "a " << s.name() << " with area " << s.area() << '\n'; } int main() { describe(Rectangle{3, 4}); describe(Circle{1}); }
a rectangle with area 12 a circle with area 3.14159
The = 0 functions are pure virtual — the next lesson explains them.
dynamic_cast
Occasionally you need to know whether a base reference really refers to a particular derived type. dynamic_cast checks at run time and returns nullptr for pointers if it doesn't:
#include <iostream> #include <memory> #include <vector> struct Node { virtual ~Node() = default; }; struct Number : Node { int value = 0; explicit Number(int v) : value(v) {} }; struct Text : Node {}; int main() { std::vector<std::unique_ptr<Node>> nodes; nodes.push_back(std::make_unique<Number>(7)); nodes.push_back(std::make_unique<Text>()); nodes.push_back(std::make_unique<Number>(35)); int sum = 0; for (const auto& n : nodes) { if (auto* num = dynamic_cast<Number*>(n.get())) sum += num->value; } std::cout << "sum of numbers: " << sum << '\n'; }
sum of numbers: 42
Frequent dynamic_casts are a design smell: usually a virtual function should do the work instead.
The cost
Each class with virtual functions has a table of function pointers (the vtable), and each object carries a hidden pointer to it. A virtual call is an indirect call that the compiler usually can't inline. That's cheap, but in very hot loops over millions of small objects, alternatives such as templates (lesson 39) or std::variant (lesson 43) can be faster.
Try it yourself
Add a Triangle class to the shapes, and compute the total area of a mixed list.
total area: 21.25