Virtual Functions & Polymorphism
At the end of the last lesson, a Vehicle& that referred to a Bus still called Vehicle::describe. Usually you want the opposite: code that works with "any vehicle" should automatically use the bus's own behaviour when it is given a bus. This is polymorphism (Greek for "many forms"): one interface, many implementations, chosen while the program runs.
In this lesson you will make functions virtual, mark overrides with override, build abstract interfaces with pure virtual functions, store different types in one container, and learn the two classic traps: slicing and missing virtual destructors.
virtual and override
Add virtual to a base class function to say "derived classes may replace this, and the replacement must be used even through a base reference or pointer". In the derived class, mark the replacement with override.
#include <print>
#include <string>
class Vehicle {
public:
explicit Vehicle(const std::string& number) : number_(number) {}
virtual ~Vehicle() = default;
virtual std::string describe() const {
return "Vehicle " + number_;
}
protected:
std::string number_;
};
class Bus : public Vehicle {
public:
Bus(const std::string& number, int seats) : Vehicle(number), seats_(seats) {}
std::string describe() const override {
return "Bus " + number_ + " with " + std::to_string(seats_) + " seats";
}
private:
int seats_;
};
void announce(const Vehicle& v) {
std::println("Now arriving: {}", v.describe());
}
int main() {
Vehicle van("RJ-14-XY-1111");
Bus city_bus("HR-12-AB-3456", 52);
announce(van);
announce(city_bus);
}Now arriving: Vehicle RJ-14-XY-1111 Now arriving: Bus HR-12-AB-3456 with 52 seats
announce knows nothing about buses, yet it prints the bus's description. Because describe is virtual, the call is decided at run time by the actual object type. This is called dynamic dispatch. Behind the scenes, each object of a class with virtual functions carries a hidden pointer to a table of function addresses (the vtable), and a virtual call looks up the right function in that table. The cost is tiny, but it exists, which is why C++ functions are not virtual unless you ask.
override is optional but strongly recommended. If the signature does not exactly match a virtual function in the base class, the compiler reports an error instead of silently creating a new, unrelated function:
class Truck : public Vehicle {
public:
using Vehicle::Vehicle;
std::string describe() override { return "Truck"; } // error: missing const,
}; // so it overrides nothingAbstract classes and interfaces
Sometimes the base class has no sensible implementation. What is the area of "a shape"? A pure virtual function, written = 0, has no body and must be overridden. A class with at least one pure virtual function is abstract: you cannot create objects of it, only of derived classes that implement everything.
#include <memory>
#include <numbers>
#include <print>
#include <string>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0; // pure virtual
virtual std::string name() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return std::numbers::pi * r_ * r_; }
std::string 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_; }
std::string name() const override { return "Rectangle"; }
private:
double w_;
double h_;
};
class Triangle : public Shape {
public:
Triangle(double base, double height) : base_(base), height_(height) {}
double area() const override { return 0.5 * base_ * height_; }
std::string name() const override { return "Triangle"; }
private:
double base_;
double height_;
};
int main() {
std::vector<std::unique_ptr<Shape>> plot;
plot.push_back(std::make_unique<Circle>(7));
plot.push_back(std::make_unique<Rectangle>(4, 2.5));
plot.push_back(std::make_unique<Triangle>(6, 3));
double total = 0;
for (const auto& shape : plot) {
std::println("{:<10} {:>8.2f}", shape->name(), shape->area());
total += shape->area();
}
std::println("{:<10} {:>8.2f}", "Total", total);
// Shape s; // error: Shape is abstract
}Circle 153.94 Rectangle 10.00 Triangle 9.00 Total 172.94
A container must hold one type, so the vector holds std::unique_ptr<Shape>: smart pointers to shapes, each owning one object of any derived type. std::make_unique<Circle>(7) creates a circle on the heap; the arrow -> calls a member through a pointer. You will study smart pointers properly in Smart Pointers vs new & delete. std::numbers::pi comes from <numbers> (C++20).
An abstract class containing only pure virtual functions and a virtual destructor, like Shape, is what other languages call an interface.
Always give a polymorphic base a virtual destructor
When an object is destroyed through a pointer to its base class, the base destructor must be virtual, or the derived part is never cleaned up. Formally, deleting a derived object through a base pointer without a virtual destructor is undefined behaviour.
#include <memory>
#include <print>
struct Logger {
virtual ~Logger() { std::println("~Logger"); }
};
struct FileLogger : Logger {
~FileLogger() override { std::println("~FileLogger: log file closed"); }
};
int main() {
std::unique_ptr<Logger> log = std::make_unique<FileLogger>();
} // log is destroyed here~FileLogger: log file closed ~Logger
With the virtual removed, a typical compiler would print only ~Logger, and the log file would never close. Rule: if a class has any virtual function, give it a virtual destructor, usually virtual ~Name() = default;.
Object slicing
Polymorphism works through references and pointers. If you copy a derived object into a base object, only the base part is copied; the rest is "sliced off".
Bus city_bus("HR-12-AB-3456", 52);
Vehicle copy = city_bus; // slicing: copy is a plain Vehicle
std::println("{}", copy.describe());Vehicle HR-12-AB-3456
This is why the shape vector stores pointers, and why functions take const Vehicle& rather than Vehicle.
Common mistakes
- Forgetting
virtualin the base class. The derived function then only hides the base one, and base references call the base version. - Small signature differences. A missing
constor a different parameter type creates a new function instead of an override;overridecatches this. - No virtual destructor in a polymorphic base. Deleting through a base pointer skips the derived destructor.
- Passing polymorphic objects by value. The object is sliced; pass by reference or store smart pointers.
Key points
- A
virtualfunction is chosen by the object's real type at run time, even through a base reference or pointer. - Mark every overriding function with
overrideso the compiler checks the signature. - A pure virtual function (
= 0) makes a class abstract; abstract classes define interfaces. - Store different derived types together as
std::vector<std::unique_ptr<Base>>. - Give every polymorphic base class a virtual destructor, and avoid slicing by using references or pointers.
Exercise
Create an abstract class Payment with a pure virtual double fee(double amount) const and std::string method() const. Derive Upi (no fee), Card (2 percent fee) and NetBanking (flat Rs. 15). Store one of each in a std::vector<std::unique_ptr<Payment>> and print the method and fee for a payment of Rs. 2500, with two decimals.
Show solution
#include <memory>
#include <print>
#include <string>
#include <vector>
class Payment {
public:
virtual ~Payment() = default;
virtual double fee(double amount) const = 0;
virtual std::string method() const = 0;
};
class Upi : public Payment {
public:
double fee(double) const override { return 0.0; }
std::string method() const override { return "UPI"; }
};
class Card : public Payment {
public:
double fee(double amount) const override { return amount * 0.02; }
std::string method() const override { return "Card"; }
};
class NetBanking : public Payment {
public:
double fee(double) const override { return 15.0; }
std::string method() const override { return "Net banking"; }
};
int main() {
std::vector<std::unique_ptr<Payment>> options;
options.push_back(std::make_unique<Upi>());
options.push_back(std::make_unique<Card>());
options.push_back(std::make_unique<NetBanking>());
for (const auto& p : options) {
std::println("{:<12} fee Rs. {:.2f}", p->method(), p->fee(2500));
}
}UPI fee Rs. 0.00 Card fee Rs. 50.00 Net banking fee Rs. 15.00
The loop never checks which kind of payment it holds; each object's own fee runs through dynamic dispatch. Unused parameters are left unnamed (double) to avoid compiler warnings.
Quick Quiz
1. What makes a call through a base reference run the derived class's version?
virtual enables dynamic dispatch; override only checks that you really override.
2. What is an abstract class?
Pure virtual functions (= 0) must be implemented by derived classes before objects can be created.
3. Why should a polymorphic base class have a virtual destructor?
Without it, destroying through a base pointer is undefined behaviour and usually skips derived cleanup.
4. What happens in Vehicle v = some_bus;?
Copying into a base object slices away the derived part; use references or pointers for polymorphism.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
