Abstract Classes and Interfaces in C++
Learn abstract classes and interfaces in C++ using pure virtual functions to design flexible and extensible software architectures.
In C++, abstract classes are classes that contain at least one pure virtual function and cannot be instantiated directly. There is no separate keyword for “interface” in C++; instead, an abstract class composed entirely of pure virtual functions serves as an interface. In this article, we’ll cover abstract class/interface design, implementing multiple interfaces, the Dependency Inversion Principle (DIP), and a practical Book scenario.
1) Pure Virtual Function and Abstract Class
A virtual function marked with = 0 is pure virtual. Such a class becomes abstract and
cannot be instantiated directly.
struct IPrintable {
virtual ~IPrintable() = default; // Virtual destructor is good practice for interfaces, too
virtual void Print() const = 0; // pure virtual → interface contract
};
struct IEntity {
virtual ~IEntity() = default;
virtual std::string Key() const = 0;
};
These two classes now act as “interfaces.” Implementing classes must provide behavior for Print() and Key().
2) Book Base: Abstract Class
Let’s express the common contract with an abstract BookBase class. Derived classes will define the pricing.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct BookBase : IPrintable, IEntity {
protected:
string title, author;
public:
BookBase(string t, string a) : title(move(t)), author(move(a)) {}
virtual ~BookBase() = default;
// Pure virtual: derived classes compute the price
virtual double FinalPrice() const = 0;
// IPrintable contract
void Print() const override {
cout << title << " - " << author
<< " | " << FinalPrice() << " TL" << endl;
}
// IEntity contract
string Key() const override { return title + ":" + author; }
};
BookBase cannot be directly new’d because FinalPrice() is pure virtual. However, we can still provide
default implementations for the shared behavior Print() and Key().
3) Classes Implementing the Interface (EBook / PrintedBook)
Now let’s create two different book types and fulfill the FinalPrice() contract.
struct EBook : BookBase {
double base, vat;
EBook(string t, string a, double b, double v)
: BookBase(move(t), move(a)), base(b), vat(v) {}
double FinalPrice() const override { return base * (1.0 + vat); }
};
enum class Cover { Soft, Hard };
struct PrintedBook : BookBase {
double base, shipping; int pages; Cover cover;
PrintedBook(string t, string a, double b, double s, int p, Cover c)
: BookBase(move(t), move(a)), base(b), shipping(s), pages(p), cover(c) {}
double FinalPrice() const override {
double cost = base + shipping + 0.04 * pages;
if (cover == Cover::Hard) cost *= 1.1;
return cost;
}
};
Both classes satisfy the BookBase interface; thus they can be used polymorphically.
4) Implementing Multiple Interfaces (Interface Segregation)
Designing small, focused interfaces instead of a single large one reduces maintenance costs.
For example, we can separate discount policy with an IDiscountable interface.
struct IDiscountable {
virtual ~IDiscountable() = default;
virtual double ApplyDiscount(double rate) const = 0; // 0.10 → 10%
};
struct DiscountedEBook : EBook, IDiscountable {
using EBook::EBook; // inherit constructors
double ApplyDiscount(double rate) const override {
return FinalPrice() * (1.0 - rate);
}
};
With this design, DiscountedEBook supports both the BookBase contract and the discount interface.
5) Dependency Inversion (DIP) and Testability
Classes should depend on abstractions (interfaces), not concrete types. This loosely couples modules and
makes unit testing easier. Below, a “pricing engine” sees only the BookBase interface.
struct PricingEngine {
double Total(const vector<shared_ptr<BookBase>>& items) const {
double sum = 0.0;
for (const auto& b : items) sum += b->FinalPrice();
return sum;
}
};
int main() {
vector<shared_ptr<BookBase>> items;
items.push_back(make_shared<EBook>("Modern C++", "A. Dev", 100, 0.1));
items.push_back(make_shared<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, Cover::Hard));
PricingEngine eng;
cout << "Total: " << eng.Total(items) << " TL" << endl;
for (const auto& b : items) b->Print(); // IPrintable
}
PricingEngine does not depend on any concrete type; it only works with the BookBase interface. Therefore,
you can write unit tests by providing a MockBook instead of a real EBook.
6) Interfaces with Multiple Inheritance
C++ supports multiple inheritance; for interfaces this is typically safe because interfaces carry no state. Using multiple inheritance with stateful base classes can introduce the diamond problem; where appropriate, use virtual inheritance or preferably composition.
struct IStorable { virtual ~IStorable()=default; virtual void Save() const =0; }
struct ISearchable{ virtual ~ISearchable()=default; virtual bool Matches(string) const =0; };
struct SearchableBook : BookBase, IStorable, ISearchable {
using BookBase::BookBase;
double FinalPrice() const override { return 0.0; } // simple example
void Save() const override { /* write to file... */ }
bool Matches(string q) const override {
return title.find(q)!=string::npos || author.find(q)!=string::npos;
}
};
7) Factory with Interfaces (Example)
We can decouple creation logic from concrete classes using a factory interface.
struct IBookFactory {
virtual ~IBookFactory() = default;
virtual shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v)=0;
virtual shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c)=0;
};
struct DefaultBookFactory : IBookFactory {
shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v) override {
return make_shared<EBook>(move(t), move(a), b, v);
}
shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c) override {
return make_shared<PrintedBook>(move(t), move(a), b, s, p, c);
}
};
High-level modules depend only on IBookFactory; concrete production can be swapped easily or mocked in tests.
8) Design Tips
- Always define a virtual destructor in interfaces.
- Keep interfaces small and focused (Interface Segregation Principle).
- Depend on abstractions, not on concrete classes (DIP).
- Prefer pointers/references for polymorphic use; copying by value can cause slicing.
- Use multiple inheritance mainly for behavior (interfaces); for shared state, composition is safer.
9) Common Pitfalls
- Forgetting the virtual destructor in an abstract class.
- Designing an interface that is too large (forcing implementers to add empty methods).
- Depending directly on concrete classes (reduces testability and flexibility).
- Combining stateful bases with multiple inheritance and introducing the diamond problem.
10) TL;DR
- In C++, an “interface” = an abstract class composed entirely of pure virtual functions.
- Abstract classes can’t be instantiated; a derived class becomes concrete by overriding the contract.
- Prefer small, focused interfaces (ISP) and depend on abstractions (DIP).
- Use pointers/references for polymorphism; don’t forget the virtual destructor.
- Use multiple inheritance for interfaces; prefer composition for shared state.
- All examples run on Visual Studio 2022 and GCC 11+.