Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Modern C++ Basics: Types, Containers, Functions and Classes
Inheritance and Polymorphism
Define a base class with virtual functions, override them in derived classes, store different shapes behind one interface using smart pointers, and avoid slicing.
What you'll build
A payroll program: different kinds of employees share one interface, and a single loop calculates pay for all of them without knowing the concrete types.
The idea
Inheritance lets a derived class reuse and specialize a base class. Polymorphism lets code work with the base interface while each object does the right thing for its real type.
The rules:
- Mark functions meant to be overridden
virtualin the base class, and mark overridesoverridein derived classes. The compiler then checks the signature matches. - A pure virtual function
virtual double pay() const = 0;makes the class abstract: it cannot be instantiated and defines an interface. - A base class used polymorphically must have a virtual destructor, otherwise deleting a derived object through a base pointer is undefined behavior.
- Store polymorphic objects through pointers or references:
std::vector<std::unique_ptr<Employee>>. Storing them by value causes slicing: the derived part is cut off. - Prefer small interfaces and composition. If a class hierarchy is deep, or derived classes override most things, composition or
std::variantis often clearer.
The code
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Employee {
public:
explicit Employee(std::string name) : name_{std::move(name)} {}
virtual ~Employee() = default; // required for polymorphic base classes
virtual double pay() const = 0; // pure virtual: Employee is abstract
virtual std::string role() const { return "employee"; }
const std::string& name() const { return name_; }
private:
std::string name_;
};
class Salaried : public Employee {
public:
Salaried(std::string name, double annual) : Employee{std::move(name)}, annual_{annual} {}
double pay() const override { return annual_ / 12.0; }
std::string role() const override { return "salaried"; }
private:
double annual_;
};
class Hourly : public Employee {
public:
Hourly(std::string name, double rate, int hours) : Employee{std::move(name)}, rate_{rate}, hours_{hours} {}
double pay() const override {
const int overtime = hours_ > 160 ? hours_ - 160 : 0;
return rate_ * (hours_ - overtime) + rate_ * 1.5 * overtime;
}
std::string role() const override { return "hourly"; }
private:
double rate_;
int hours_;
};
class Contractor : public Employee {
public:
Contractor(std::string name, double fee) : Employee{std::move(name)}, fee_{fee} {}
double pay() const override { return fee_; }
// role() not overridden: uses the base class default
private:
double fee_;
};
int main() {
std::vector<std::unique_ptr<Employee>> staff;
staff.push_back(std::make_unique<Salaried>("Ada", 84000));
staff.push_back(std::make_unique<Hourly>("Grace", 30.0, 170));
staff.push_back(std::make_unique<Contractor>("Linus", 5000));
double total = 0;
for (const auto& e : staff) {
std::cout << e->name() << " (" << e->role() << "): " << e->pay() << '\n'; // virtual dispatch
total += e->pay();
}
std::cout << "monthly total: " << total << '\n';
// Employee e{"x"}; // error: cannot instantiate an abstract class
}
Output
Ada (salaried): 7000 Grace (hourly): 5250 Linus (employee): 5000 monthly total: 17250
Run this on your computer — see “Get Started” for setup.
One loop handles three different classes. Adding a Manager class requires no change to the loop; that is the open/closed principle in practice.
Common mistakes
- A non-virtual destructor in a polymorphic base class. Deleting through a base pointer then skips the derived destructor.
- Forgetting
override. A small signature difference (const, parameter type) creates a new function instead of overriding, silently. - Slicing.
std::vector<Employee>orEmployee e = hourly;copies only the base part. Use pointers or references. - Calling virtual functions from constructors or destructors. The derived part isn't built (or is already gone), so the base version runs.
- Inheriting to reuse code. Use inheritance for "is-a" relationships; use members for "has-a".
Key points
- Mark overridable functions
virtual, overridesoverride, and give polymorphic bases a virtual destructor. - Pure virtual functions define abstract interfaces.
- Hold polymorphic objects through
std::unique_ptr(or references), never by value. - Prefer composition when the relationship isn't truly "is-a".
Further reading: C++ Core Guidelines: Class hierarchies.
Practice questions
Why does a polymorphic base class need a virtual destructor?
Answer
If you delete a derived object through a base pointer and the destructor isn't virtual, only the base destructor runs. The derived part is never destroyed, which is undefined behavior and can leak resources.
What is object slicing?
Answer
Copying a derived object into a base-class object by value keeps only the base subobject and discards the derived members. Avoid it by storing and passing polymorphic objects through pointers or references.
Open in the app to mark lessons complete, take notes and track your progress.