8.6 Polymorphism
Polymorphism means "many forms." In C++, runtime polymorphism lets the same call choose different behavior based on the real object type.
The important ingredients are:
- a base class with at least one
virtualfunction; - derived classes that override that function;
- a call through a base pointer or base reference.
Without the pointer or reference, C++ may treat the object as the static type you stored. Without virtual, C++ uses the function selected at compile time.
Virtual dispatch through base pointers
Imagine a payroll screen that should print pay for many employee types. The caller should not need an if statement for every employee kind.
class Employee {
public:
virtual ~Employee() = default;
virtual std::string name() const = 0;
virtual double pay() const = 0;
};
class FullTimeEmployee : public Employee {
private:
std::string name_;
double monthly_salary_;
public:
FullTimeEmployee(std::string name, double monthly_salary)
: name_(name), monthly_salary_(monthly_salary) {}
std::string name() const override { return name_; }
double pay() const override { return monthly_salary_; }
};Now the caller can use one interface:
FullTimeEmployee alice("Alice", 5000.0);
PartTimeEmployee bob("Bob", 40.0, 80);
Employee* employees[] = { &alice, &bob };
for (Employee* employee : employees) {
std::cout << employee->name() << ": " << employee->pay() << std::endl;
}The expression employee->pay() dispatches to the override for the real object. If employee points to FullTimeEmployee, it calls FullTimeEmployee::pay. If it points to PartTimeEmployee, it calls PartTimeEmployee::pay.
Object slicing
Polymorphism depends on preserving the real derived object. If you copy a derived object into a base object by value, the derived part is sliced away.
class Product {
public:
virtual std::string label() const {
return "generic product";
}
};
class Food : public Product {
public:
std::string label() const override {
return "fresh food";
}
};
Food food;
Product copy = food; // slicing: only the Product part is copied
std::cout << copy.label(); // prints generic productTo keep polymorphism, use references, pointers, or smart pointers:
Food food;
Product& ref = food;
std::cout << ref.label(); // prints fresh foodThe object did not change. The way you stored it changed whether C++ could see the derived behavior.
Virtual destructors and interface cleanup
If a base class is used polymorphically, its destructor should usually be virtual.
class Exporter {
public:
virtual ~Exporter() = default;
virtual std::string export_line(const std::string& value) const = 0;
};This matters when objects are deleted through base pointers.
Exporter* exporter = new JsonExporter();
delete exporter; // safe only if Exporter has a virtual destructorIn modern C++, you often use smart pointers instead of raw new and delete, but the principle is the same. The base class must allow cleanup to reach the derived object. A polymorphic base without a virtual destructor is a common source of resource bugs.
Polymorphism is most effective when the base interface is small and meaningful. Instead of asking "what exact type is this?", caller code asks "what behavior do I need?"
void save_line(const Exporter& exporter, const std::string& value) {
std::cout << exporter.export_line(value) << std::endl;
}