Abstract Classes and Interfaces

An abstract class defines what a family of types can do without saying how. It has at least one pure virtual function and can't be instantiated; derived classes must implement the missing pieces. A class with only pure virtual functions is an interface — the main tool for decoupling parts of a program.

Pure virtual functions

C++
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Notifier {                                   // an interface
public:
    virtual ~Notifier() = default;
    virtual void send(const std::string& to, const std::string& message) = 0;
};

class EmailNotifier : public Notifier {
public:
    void send(const std::string& to, const std::string& message) override {
        std::cout << "email to " << to << ": " << message << '\n';
    }
};

class SmsNotifier : public Notifier {
public:
    void send(const std::string& to, const std::string& message) override {
        std::cout << "SMS to " << to << ": " << message.substr(0, 20) << '\n';
    }
};

int main() {
    std::vector<std::unique_ptr<Notifier>> channels;
    channels.push_back(std::make_unique<EmailNotifier>());
    channels.push_back(std::make_unique<SmsNotifier>());
    for (auto& c : channels) c->send("Sam", "Your order #1042 has been shipped");
}
Output
email to Sam: Your order #1042 has been shipped
SMS to Sam: Your order #1042 has

= 0 marks a function as pure virtual. A class with any pure virtual function is abstract:

C++
#include <string>

class Notifier {
public:
    virtual ~Notifier() = default;
    virtual void send(const std::string& to, const std::string& message) = 0;
};

int main() {
    Notifier n;
}
Compiler error
main.cpp:10:14: error: variable type 'Notifier' is an abstract class

Programming to an interface

Code that depends on an interface rather than a concrete class can be reused and tested easily. Here, the order service doesn't know or care how notifications are delivered — and a test can pass in a fake that just records messages:

C++
#include <iostream>
#include <string>
#include <vector>

class Notifier {
public:
    virtual ~Notifier() = default;
    virtual void send(const std::string& to, const std::string& message) = 0;
};

class OrderService {
public:
    explicit OrderService(Notifier& notifier) : notifier_(notifier) {}
    void ship(const std::string& customer, int order_id) {
        // ... update stock, print a label ...
        notifier_.send(customer, "Order " + std::to_string(order_id) + " shipped");
    }
private:
    Notifier& notifier_;
};

class FakeNotifier : public Notifier {                 // for testing
public:
    void send(const std::string& to, const std::string& message) override {
        sent.push_back(to + ": " + message);
    }
    std::vector<std::string> sent;
};

int main() {
    FakeNotifier fake;
    OrderService service{fake};
    service.ship("Ana", 7);
    service.ship("Ben", 8);
    for (const auto& s : fake.sent) std::cout << s << '\n';
}
Output
Ana: Order 7 shipped
Ben: Order 8 shipped

Passing the dependency into the constructor like this is called dependency injection.

Abstract classes with shared code

An abstract class can also contain ordinary members and implemented functions. A common pattern is the template method: the base class defines the overall algorithm and derived classes fill in steps:

C++
#include <iostream>
#include <string>

class Report {
public:
    virtual ~Report() = default;
    void print() const {                              // the fixed structure
        std::cout << "=== " << title() << " ===\n";
        body();
        std::cout << "--- end ---\n";
    }
protected:
    virtual std::string title() const = 0;            // the customisable steps
    virtual void body() const = 0;
};

class SalesReport : public Report {
protected:
    std::string title() const override { return "Sales, March"; }
    void body() const override { std::cout << "Revenue: 12,400\n"; }
};

int main() {
    SalesReport r;
    r.print();
}
Output
=== Sales, March ===
Revenue: 12,400
--- end ---

Interfaces and templates

Abstract classes give run-time polymorphism: the choice of implementation can change while the program runs. Templates and concepts (lessons 39–41) give compile-time polymorphism — no virtual calls, but the type must be known when compiling. Use interfaces when you need to store different types together or swap implementations at run time.

Try it yourself

Define a PaymentMethod interface with pay(double), implement it for a card and a voucher with a limited balance, and pay a few bills.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
12 paid by voucher
18 paid by card
9.5 paid by voucher