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.

Create a free account to save this page, take notes and track your progress. Sign in

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 virtual in the base class, and mark overrides override in 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::variant is often clearer.

The code

An Employee interface with polymorphic pay
#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> or Employee 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, overrides override, 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.

Was this page helpful?