Abstraction

Definition: Abstraction exposes a stable interface and hides implementation details. Users depend on what the type does (the contract), not how it does it (the implementation).


Key Benefits

Simplicity

Small public surface with clear semantics

Modularity

Independent evolution of internals without breaking clients

Maintainability

Fewer ripple effects across codebase

Testability

Mock the interface, validate behavior independently

Flexibility

Swap implementations without changing client code


When to Use Abstract Classes

Create abstract classes when you want to:

Define a Contract

Multiple concrete types must follow the same interface

Enable Polymorphism

Different implementations with common behavior

Prevent Direct Instantiation

Incomplete or conceptual types shouldn’t be created

Share Common Behavior

Provide base functionality while requiring specialization

Design Plugin Architectures

Clients provide their own implementations

Create Frameworks

Define extension points for users

Example Scenarios

class Device {
public:
    virtual ~Device() = default;
    virtual void initialize() = 0;
    virtual void read(Buffer& buf) = 0;
    virtual void write(const Buffer& buf) = 0;
};
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    virtual void draw() const = 0;
};
class DataSource {
public:
    virtual ~DataSource() = default;
    virtual bool open() = 0;
    virtual Data read() = 0;
    virtual bool write(const Data& d) = 0;
    virtual void close() = 0;
};
class Widget {
public:
    virtual ~Widget() = default;
    virtual void render() = 0;
    virtual void handle_event(const Event& e) = 0;
    virtual void resize(int w, int h) = 0;
};

How is Abstraction Implemented in C++?

Abstract Classes

Define behavior as a contract using pure virtual methods

Pure Virtual Methods

Enforce implementation in derived types (= 0)

Encapsulation

Keep data private/protected; expose intent through methods

Example:

Listing 27 Abstract Vehicle Class
class Vehicle {
public:
    virtual ~Vehicle() = default;

    // Pure virtual - MUST be implemented in derived classes
    virtual void drive() = 0;

    // Pure virtual - MUST be implemented in derived classes
    virtual void update_location(const Location& location) = 0;
};

Pure virtual Methods (= 0)

Definition: A pure virtual method is declared with = 0 and:

  • Makes the class abstract — cannot be instantiated

  • Requires derived classes to implement the method

  • Defines a contract that concrete classes must fulfill

  • Can optionally provide a default implementation (rare)

Basic Example

Listing 28 Pure Virtual Method Declaration
class Vehicle {
public:
    virtual ~Vehicle() = default;
    virtual void drive() = 0;  // Pure virtual method
};
int main(){
    // transportation::Vehicle v;  // ERROR: cannot instantiate abstract class
}
Listing 29 Mandatory base class method overriding
class RoboTaxi : public Vehicle {
public:
    void drive() override;

    /* robo_taxi.cpp
    {
      // logic
    }
    */
};
int main(){
    RoboTaxi rt;  // OK - RoboTaxi has overridden drive()
}

Note

A caller invokes vehicle->drive() without knowledge of internal details. Any conforming concrete type satisfies the same contract.

Abstract Class Requirements

A class is abstract if it has at least one pure virtual method. An abstract class:

Cannot be instantiated directly

Can have constructors (for derived classes to call)

Can have data members (typically protected)

Can mix pure and regular virtual methods

Can have non-virtual methods

Listing 30 Abstract Class with Mixed Members
class Shape {
protected:
    Point center_;  // Data member

public:
    // Constructor (for derived classes)
    Shape(const Point& center) : center_{center} {}

    // Pure virtual - must override
    virtual double area() const = 0;
    virtual void draw() const = 0;

    // Regular virtual - can override
    virtual void move(const Vector& offset);
    /* shape.cpp
    {
        center_ = center_ + offset;
    }*/

    // Non-virtual - cannot override
    [[nodiscard]] Point get_center() const noexcept{ return center_; }

    // Virtual destructor
    virtual ~Shape() = default;
};

Pure Virtual with Default Implementation

💡 Advanced Pattern: Pure Virtual with Implementation

A pure virtual method can optionally provide a default implementation that derived classes can call explicitly:

class Logger {
public:
    virtual ~Logger() = default;

    // Pure virtual with implementation
    virtual void log(const std::string& message) = 0 {
        // Default implementation
        std::cout << "[LOG] " << message << '\n';
    }
};

class FileLogger : public Logger {
public:
    void log(const std::string& message) override;
    /* file_logger.cpp
    {
        // Can call base implementation
        Logger::log(message);
        // Add file-specific behavior
        write_to_file(message);
    }*/
};

Note

This pattern is rare but useful when you want to enforce implementation while providing a fallback.


Concrete Classes

Definition: A concrete class is a class that can be instantiated. It provides complete implementations for all inherited pure virtual methods.

Key Characteristics

Fully Defined

Implements all inherited pure virtual methods

Instantiable

Can create objects directly

Observable

Provides complete, working functionality

Safe to Use

All methods have defined behavior

Example: Complete Hierarchy

Listing 31 Fully Concrete Class
// Abstract base - defines interface contract
class Vehicle {
public:
    virtual ~Vehicle() = default;
    virtual void drive() = 0;
    virtual void stop() = 0;
};

// Concrete derived class - implements all pure virtuals
class RoboTaxi : public Vehicle {
public:
    ~RoboTaxi() override = default;

    void drive() override;
    /* robo_taxi.cpp
    {
        // RoboTaxi-specific driving logic
    }*/

    void stop() override;
    /* robo_taxi.cpp
    {
        // RoboTaxi-specific stopping logic
    }*/
};
 int main(){
     RoboTaxi robo_taxi{"ROBOT001", 6};  // OK - fully concrete
     robo_taxi.drive();  // OK
}
Listing 32 Still Abstract - Missing Implementation
// Abstract base
class Vehicle {
public:
    virtual ~Vehicle() = default;
    virtual void drive() = 0;
    virtual void stop() = 0;
};

// Still abstract - missing stop() implementation
class Taxi : public Vehicle {
public:
    ~Taxi() override = default;
    void drive() override;
    /* taxi.cpp
    {
     // Taxi-specific driving logic
    }*/

    // Missing: void stop() override { }
};
 int main(){
     // ERROR: cannot instantiate abstract class
     // Taxi taxi{"TAXI001", 6};
}

Warning

A derived class must implement ALL pure virtual methods from the base class to become concrete. We forgot to implement stop() and therefore, Taxi is still an abstract class.


Design Patterns with Abstraction

Listing 33 Interchangeable Algorithms
// Abstract strategy interface
class SortStrategy {
public:
    virtual ~SortStrategy() = default;
    virtual void sort(std::vector<int>& data) = 0;
};

// Concrete strategies
class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override;
    /* quick_sort.cpp
    {
        // QuickSort implementation
    }*/
};

class MergeSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override;
    /* merge_sort.cpp
    {
        // MergeSort implementation
    }*/
};

// Client uses abstraction
class DataProcessor {
private:
    std::unique_ptr<SortStrategy> strategy_;
public:
    void set_strategy(std::unique_ptr<SortStrategy> strategy) {
        strategy_ = std::move(strategy);
    }

    void process(std::vector<int>& data);
    /* data_processor.cpp
    {
        strategy_->sort(data);  // Uses abstract interface
    }*/
};
Listing 34 Object Creation Abstraction
// Abstract product
class Document {
public:
    virtual ~Document() = default;
    virtual void open() = 0;
    virtual void save() = 0;
};

// Concrete products
class PDFDocument : public Document {
public:
    void open() override;
    /* pdf_document.cpp
    {
      // PDF opening logic
    */}

    void save() override;
    /* pdf_document.cpp
    {
      // PDF saving logic
    }*/
};

class WordDocument : public Document {
public:
    void open() override;
    /* word_document.cpp
    {
      // Word opening logic
    }*/

    void save() override;
    /*  word_document.cpp
    {
      // Word saving logic
    }*/
};

// Factory creates objects polymorphically
class DocumentFactory {
public:
    static std::unique_ptr<Document> create(const std::string& type) {
        if (type == "pdf") {
            return std::make_unique<PDFDocument>();
        } else if (type == "word") {
            return std::make_unique<WordDocument>();
        }
        return nullptr;
    }
};
Listing 35 Event Notification System
// Abstract observer interface
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& event) = 0;
};

// Concrete observers
class EmailNotifier : public Observer {
public:
    void update(const std::string& event) override;
    /*  email_notifier.cpp
    {
      send_email("Event occurred: " + event);
    }*/
};

class LoggerObserver : public Observer {
public:
    void update(const std::string& event) override;
    /*  logger_observer.cpp
    {
      // log_to_file("Event occurred: " + event);
    }*/
};

// Subject maintains list of observers
class EventManager {
private:
    std::vector<Observer*> observers_;
public:
    void attach(Observer* obs);
    /*  event_manager.cpp
    {
      observers_.push_back(obs);
    }*/


    void notify(const std::string& event);
    /*  event_manager.cpp
    {
      for (auto* obs : observers_) {
            obs->update(event);  // Polymorphic call
        }
    }*/
};

Best Practices

1. Minimal Interfaces

Keep interfaces minimal and focused

Show Example
// Good - focused interface
class Drawable {
public:
    virtual ~Drawable() = default;
    virtual void draw() const = 0;
};

// Avoid - interface too broad
class GraphicsObject {
public:
    virtual void draw() const = 0;
    virtual void rotate(double angle) = 0;
    virtual void scale(double factor) = 0;
    virtual void translate(const Vector& offset) = 0;
    virtual void set_color(const Color& color) = 0;
    // Too many responsibilities!
};
2. Contracts, Not Implementation

Use abstract classes for contracts, not implementation sharing

  • If you need shared implementation, consider composition

  • Abstract classes define “what”, not “how”

3. Prefer Pure Virtual

Prefer pure virtual over virtual with defaults

  • Forces derived classes to think about implementation

  • Avoids silent bugs from forgotten overrides

4. Virtual Destructors

Always make destructors virtual

Show Example
class AbstractBase {
public:
    virtual ~AbstractBase() = default;  // Essential!
    virtual void do_something() = 0;
};
5. Interface Segregation

Many small interfaces beat one large interface (ISP)

  • Clients shouldn’t depend on methods they don’t use

6. Document Contracts

Document the contract clearly

Show Example
class DataSource {
public:
    virtual ~DataSource() = default;

    /**
     * Reads data from the source.
     *
     * @return Data buffer, or empty on error
     * @throws std::runtime_error if source is not open
     */
    virtual std::vector<uint8_t> read() = 0;
};

Common Pitfalls

❌ Pitfall 1: Too Many Pure Virtuals
// Avoid - forces too much implementation
class Vehicle {
public:
    virtual void start_engine() = 0;
    virtual void stop_engine() = 0;
    virtual void open_door() = 0;
    virtual void close_door() = 0;
    virtual void honk_horn() = 0;
    virtual void turn_lights_on() = 0;
    virtual void turn_lights_off() = 0;
    // ... 50 more methods
};

Fix: Break into smaller, focused interfaces (Interface Segregation Principle).

❌ Pitfall 2: Abstract Class with Data Members
// Questionable - mixes interface and implementation
class Shape {
private:
    Color color_;  // Implementation detail in abstract class?
public:
    virtual double area() const = 0;
};

Better: Keep abstract classes focused on interface, use composition for shared data.

❌ Pitfall 3: Forgetting Virtual Destructor
class Base {
public:
    // Missing: virtual ~Base() = default;
    virtual void foo() = 0;
};

Always add virtual destructor to abstract base classes to prevent resource leaks.


Core Concepts:

  • Abstraction exposes interface, hides implementation

  • Abstract classes define contracts using pure virtual methods

  • Pure virtual methods (= 0) must be implemented by derived classes

  • Concrete classes implement all inherited pure virtuals

  • Always provide virtual destructors in abstract base classes

  • Keep interfaces minimal and focused

  • Use abstraction for polymorphic behavior and dependency inversion

  • Abstract classes cannot be instantiated directly

  • Derived classes remain abstract until all pure virtuals are implemented

Contract

Define what, not how

Polymorphism

Enable flexible designs

Encapsulation

Hide implementation details