Loading...

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


9) Common Pitfalls


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+.

Related Articles