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
#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"); }
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:
#include <string> class Notifier { public: virtual ~Notifier() = default; virtual void send(const std::string& to, const std::string& message) = 0; }; int main() { Notifier n; }
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:
#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'; }
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:
#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(); }
=== 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.
12 paid by voucher 18 paid by card 9.5 paid by voucher