Module 7.4

C++ Design Patterns

Learn battle-tested solutions to common software design problems. Master creational, structural, and behavioral patterns that make your C++ code more flexible, maintainable, and professional!

45 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Singleton - controlled single instances
  • Factory - flexible object creation
  • Observer - event-driven communication
  • Strategy - interchangeable algorithms
  • Modern C++ idioms (RAII, CRTP)
Contents
01

Creational Patterns

Creational patterns deal with object creation mechanisms. They help you create objects in a manner suitable for the situation, making your code more flexible and reusable.

What are Design Patterns?

Think of design patterns as proven recipes for solving common programming problems. Just like a chef doesn't reinvent how to make a sauce every time, programmers use design patterns as templates for solving recurring design challenges. These patterns were popularized by the "Gang of Four" (GoF) in their influential book "Design Patterns: Elements of Reusable Object-Oriented Software" (1994).

Design patterns are not code you can copy-paste. They are descriptions or templates for how to solve a problem that can be used in many different situations. Learning patterns helps you communicate with other developers using a shared vocabulary and write code that's easier to maintain and extend.

Concept

Design Pattern

A design pattern is a general, reusable solution to a commonly occurring problem in software design. It's not a finished design that can be transformed directly into code - it's a template for how to solve a problem that can be applied in many situations.

Design patterns were popularized by the "Gang of Four" (GoF) in their influential 1994 book. They provide a shared vocabulary for developers and help create code that's easier to maintain and extend.

Three categories: Creational (object creation), Structural (object composition), Behavioral (object interaction and responsibility).

The Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. Think of it like having only one president of a country, only one sun in our solar system, or only one configuration manager in your application.

When would you use Singleton? Common examples include logging systems (you want all log messages to go to the same place), configuration managers (one source of truth for settings), connection pools (manage a shared pool of database connections), and hardware interface access (only one object should control the printer).

#include <iostream>
#include <string>
#include <mutex>

class Logger {
public:
    // Delete copy constructor and assignment operator
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
The Singleton pattern begins by preventing object duplication. The copy constructor and assignment operator are explicitly deleted using = delete, making it impossible to copy or assign Logger instances. This is crucial because the entire point of Singleton is having exactly one instance - allowing copies would defeat the purpose. These deleted functions will trigger compile-time errors if anyone tries to copy the Logger.
    // Static method to get the single instance
    static Logger& getInstance() {
        static Logger instance;  // Thread-safe in C++11 and later
        return instance;
    }
The getInstance() static method is the global access point to the single Logger instance. It uses the "Meyers Singleton" technique with a static local variable that's initialized on first call. In C++11 and later, this initialization is guaranteed to be thread-safe - if multiple threads call getInstance() simultaneously, the initialization happens exactly once. The instance persists for the program's lifetime and is automatically destroyed at program termination.
    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[LOG] " << message << std::endl;
    }
The log() method is thread-safe thanks to std::lock_guard, which automatically acquires the mutex when created and releases it when destroyed (RAII). This ensures that even if multiple threads log simultaneously, their messages won't interleave. The lock_guard provides exception safety - even if an exception occurs, the mutex is properly released.
private:
    Logger() {
        std::cout << "Logger initialized" << std::endl;
    }
    
    std::mutex mutex_;
};
The private constructor prevents direct instantiation - you can't write Logger myLogger;. The only way to obtain a Logger is through getInstance(). The constructor prints a message to demonstrate that it's called exactly once, no matter how many times you call getInstance(). The private mutex_ member protects the log method from concurrent access issues.
int main() {
    // Both references point to the SAME instance
    Logger& logger1 = Logger::getInstance();
    Logger& logger2 = Logger::getInstance();
    
    logger1.log("First message");
    logger2.log("Second message");
    
    // Verify they're the same instance
    std::cout << "Same instance? " 
              << (&logger1 == &logger2 ? "Yes" : "No") << std::endl;
    
    return 0;
}
// Output:
// Logger initialized
// [LOG] First message
// [LOG] Second message
// Same instance? Yes
This example demonstrates the Singleton guarantee: logger1 and logger2 are references to the exact same object, proven by comparing their memory addresses. The "Logger initialized" message appears only once, confirming single instantiation. Both variables can call log(), and all log messages go to the same Logger instance. This is the Singleton pattern's core benefit - a globally accessible, single point of control.
Singleton Criticism: While useful, Singletons are often overused. They introduce global state, make unit testing difficult, and can hide dependencies. Consider dependency injection as an alternative when appropriate.

The Factory Pattern

The Factory pattern provides an interface for creating objects without specifying their exact classes. Instead of calling new ConcreteClass() directly, you ask a factory to create the object for you. This decouples object creation from the code that uses the objects.

Think of a pizza shop: you don't go into the kitchen and make the pizza yourself. You tell the counter what type you want ("pepperoni" or "vegetarian"), and the shop (factory) creates the right pizza for you. The factory handles all the creation details.

#include <iostream>
#include <memory>
#include <string>

// Abstract product
class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
    virtual double area() const = 0;
};
The Shape class is an abstract base class that defines the interface all concrete shapes must implement. It declares two pure virtual functions: draw() for rendering the shape and area() for calculating its area. This establishes a contract ensuring all shape types can be used interchangeably through the same interface. The virtual destructor ensures proper cleanup when deleting shapes through base class pointers.
// Concrete product - Circle
class Circle : public Shape {
public:
    explicit Circle(double radius) : radius_(radius) {}
    
    void draw() const override {
        std::cout << "Drawing Circle with radius " << radius_ << std::endl;
    }
    
    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
    
private:
    double radius_;
};
The Circle class is a concrete implementation of the Shape interface. It stores a radius value and provides specific implementations for drawing (displaying circle info) and calculating area using the formula πr². The explicit keyword on the constructor prevents implicit type conversions, ensuring circles are created intentionally. This class encapsulates all circle-specific behavior while conforming to the Shape contract, allowing it to be used polymorphically.
// Concrete product - Rectangle
class Rectangle : public Shape {
public:
    Rectangle(double width, double height) : width_(width), height_(height) {}
    
    void draw() const override {
        std::cout << "Drawing Rectangle " << width_ << "x" << height_ << std::endl;
    }
    
    double area() const override {
        return width_ * height_;
    }
    
private:
    double width_, height_;
};
The Rectangle class implements Shape with width and height dimensions. It calculates area using simple multiplication (width × height) and displays rectangle dimensions when drawn. Each concrete shape class has its own constructor parameters and internal representation, but all expose the same public interface defined by Shape. This polymorphic design allows treating different shapes uniformly through the base class pointer.
// Factory class
class ShapeFactory {
public:
    enum class ShapeType { Circle, Rectangle, Triangle };
    
    static std::unique_ptr<Shape> createShape(ShapeType type, 
                                               double param1, 
                                               double param2 = 0) {
        switch (type) {
            case ShapeType::Circle:
                return std::make_unique<Circle>(param1);
            case ShapeType::Rectangle:
                return std::make_unique<Rectangle>(param1, param2);
            default:
                return nullptr;
        }
    }
};
The ShapeFactory centralizes object creation logic behind a clean interface. Clients specify a ShapeType enum value and parameters, and the factory handles instantiation details. The static createShape() method uses a switch statement to create the appropriate concrete type, returning it as a std::unique_ptr<Shape> for automatic memory management. This decouples client code from concrete classes - you can add new shape types by modifying only the factory, not every place that creates shapes.
int main() {
    // Client code doesn't need to know about concrete classes
    auto circle = ShapeFactory::createShape(
        ShapeFactory::ShapeType::Circle, 5.0);
    auto rectangle = ShapeFactory::createShape(
        ShapeFactory::ShapeType::Rectangle, 4.0, 6.0);
    
    circle->draw();     // Drawing Circle with radius 5
    rectangle->draw();  // Drawing Rectangle 4x6
    
    std::cout << "Circle area: " << circle->area() << std::endl;
    std::cout << "Rectangle area: " << rectangle->area() << std::endl;
    
    return 0;
}
The client code demonstrates the factory's power - it creates shapes without mentioning Circle or Rectangle classes explicitly. All interaction happens through the Shape interface and ShapeFactory. The code uses auto for type deduction since the factory returns smart pointers. This approach makes the code flexible and maintainable: adding a new Triangle shape only requires updating the factory, not rewriting client code. The pattern follows the Open/Closed Principle (open for extension, closed for modification).

The Builder Pattern

The Builder pattern separates the construction of a complex object from its representation. It's especially useful when an object has many optional parameters or requires multiple steps to create. Instead of a constructor with 10 parameters, you build the object step by step.

Think of ordering a custom computer: you specify the CPU, then RAM, then storage, then graphics card - each step is optional, and you can configure in any order. At the end, you call "build" and get your finished computer.

#include <iostream>
#include <string>
#include <optional>

class Computer {
public:
    void showSpecs() const {
        std::cout << "=== Computer Specs ===" << std::endl;
        std::cout << "CPU: " << cpu_ << std::endl;
        std::cout << "RAM: " << ram_gb_ << " GB" << std::endl;
        std::cout << "Storage: " << storage_gb_ << " GB" << std::endl;
        if (gpu_) {
            std::cout << "GPU: " << *gpu_ << std::endl;
        }
        std::cout << "Has WiFi: " << (has_wifi_ ? "Yes" : "No") << std::endl;
    }
    
private:
    std::string cpu_ = "Unknown";
    int ram_gb_ = 8;
    int storage_gb_ = 256;
    std::optional<std::string> gpu_;
    bool has_wifi_ = false;
    
    friend class ComputerBuilder;  // Builder can access private members
};
The Computer class has many fields, some optional (like the GPU which uses std::optional). Instead of creating dozens of constructors for every possible combination (Computer with GPU, Computer without GPU, Computer with WiFi, etc.), we use a Builder that allows setting each field individually. The friend declaration grants the builder access to Computer's private members, enabling direct field modification while keeping the Computer class itself immutable to outside code. This approach eliminates constructor explosion and makes the code far more maintainable.
class ComputerBuilder {
public:
    ComputerBuilder& setCPU(const std::string& cpu) {
        computer_.cpu_ = cpu;
        return *this;  // Return *this for method chaining
    }
    
    ComputerBuilder& setRAM(int gb) {
        computer_.ram_gb_ = gb;
        return *this;
    }
    
    ComputerBuilder& setStorage(int gb) {
        computer_.storage_gb_ = gb;
        return *this;
    }
    
    ComputerBuilder& setGPU(const std::string& gpu) {
        computer_.gpu_ = gpu;
        return *this;
    }
    
    ComputerBuilder& enableWiFi() {
        computer_.has_wifi_ = true;
        return *this;
    }
    
    Computer build() {
        return std::move(computer_);
    }
    
private:
    Computer computer_;
};
Each setter method returns a reference to the builder itself (return *this;), which enables the elegant fluent interface pattern you see in the usage example. This means you can chain multiple setter calls in a single statement like builder.setX().setY().setZ(). The build() method finalizes construction and returns the finished Computer object using std::move for efficiency (avoiding unnecessary copies). This fluent style makes the code read almost like natural language and clearly shows what's being configured.
int main() {
    // Fluent interface - chain method calls
    Computer gaming_pc = ComputerBuilder()
        .setCPU("Intel i9-13900K")
        .setRAM(32)
        .setStorage(2000)
        .setGPU("RTX 4090")
        .enableWiFi()
        .build();
    
    Computer office_pc = ComputerBuilder()
        .setCPU("Intel i5-13400")
        .setRAM(16)
        .setStorage(512)
        .build();
    
    gaming_pc.showSpecs();
    std::cout << std::endl;
    office_pc.showSpecs();
    
    return 0;
}

Practice Questions: Creational Patterns

Problem: Create a Singleton class called ConfigManager that stores application settings as key-value pairs (using std::map).

Requirements:

  • Thread-safe getInstance() method
  • Methods: set(key, value) and get(key)
  • Return empty string if key not found
Show Solution
#include <iostream>
#include <string>
#include <map>
#include <mutex>

class ConfigManager {
public:
    ConfigManager(const ConfigManager&) = delete;
    ConfigManager& operator=(const ConfigManager&) = delete;
    
    static ConfigManager& getInstance() {
        static ConfigManager instance;
        return instance;
    }
    
    void set(const std::string& key, const std::string& value) {
        std::lock_guard<std::mutex> lock(mutex_);
        config_[key] = value;
    }
    
    std::string get(const std::string& key) const {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = config_.find(key);
        return (it != config_.end()) ? it->second : "";
    }
    
private:
    ConfigManager() = default;
    std::map<std::string, std::string> config_;
    mutable std::mutex mutex_;
};

int main() {
    auto& config = ConfigManager::getInstance();
    config.set("app_name", "MyApp");
    config.set("version", "1.0.0");
    
    std::cout << config.get("app_name") << std::endl;  // MyApp
    std::cout << config.get("missing") << std::endl;   // (empty)
}

Problem: Implement a Factory pattern for creating different vehicle types.

Requirements:

  • Abstract Vehicle class with describe() and wheels()
  • Concrete classes: Car (4 wheels), Motorcycle (2 wheels), Truck (6 wheels)
  • VehicleFactory with createVehicle(type) method
Show Solution
#include <iostream>
#include <memory>
#include <string>

class Vehicle {
public:
    virtual ~Vehicle() = default;
    virtual void describe() const = 0;
    virtual int wheels() const = 0;
};

class Car : public Vehicle {
public:
    void describe() const override {
        std::cout << "Car - comfortable 4-door sedan" << std::endl;
    }
    int wheels() const override { return 4; }
};

class Motorcycle : public Vehicle {
public:
    void describe() const override {
        std::cout << "Motorcycle - fast two-wheeler" << std::endl;
    }
    int wheels() const override { return 2; }
};

class Truck : public Vehicle {
public:
    void describe() const override {
        std::cout << "Truck - heavy cargo vehicle" << std::endl;
    }
    int wheels() const override { return 6; }
};

class VehicleFactory {
public:
    enum class Type { Car, Motorcycle, Truck };
    
    static std::unique_ptr<Vehicle> createVehicle(Type type) {
        switch (type) {
            case Type::Car: return std::make_unique<Car>();
            case Type::Motorcycle: return std::make_unique<Motorcycle>();
            case Type::Truck: return std::make_unique<Truck>();
        }
        return nullptr;
    }
};

int main() {
    auto car = VehicleFactory::createVehicle(VehicleFactory::Type::Car);
    auto bike = VehicleFactory::createVehicle(VehicleFactory::Type::Motorcycle);
    
    car->describe();   // Car - comfortable 4-door sedan
    bike->describe();  // Motorcycle - fast two-wheeler
    
    std::cout << "Car wheels: " << car->wheels() << std::endl;  // 4
}

Problem: Create an HTTPRequest class with a Builder for constructing HTTP requests.

Requirements:

  • Fields: method (GET/POST), url, headers (map), body (optional)
  • Builder with fluent interface
  • Method toString() to display the request
Show Solution
#include <iostream>
#include <string>
#include <map>
#include <optional>
#include <sstream>

class HTTPRequest {
public:
    std::string toString() const {
        std::ostringstream oss;
        oss << method_ << " " << url_ << " HTTP/1.1\n";
        for (const auto& [key, value] : headers_) {
            oss << key << ": " << value << "\n";
        }
        if (body_) {
            oss << "\n" << *body_;
        }
        return oss.str();
    }
    
private:
    std::string method_ = "GET";
    std::string url_;
    std::map<std::string, std::string> headers_;
    std::optional<std::string> body_;
    
    friend class HTTPRequestBuilder;
};

class HTTPRequestBuilder {
public:
    HTTPRequestBuilder& setMethod(const std::string& method) {
        request_.method_ = method;
        return *this;
    }
    
    HTTPRequestBuilder& setURL(const std::string& url) {
        request_.url_ = url;
        return *this;
    }
    
    HTTPRequestBuilder& addHeader(const std::string& key, 
                                   const std::string& value) {
        request_.headers_[key] = value;
        return *this;
    }
    
    HTTPRequestBuilder& setBody(const std::string& body) {
        request_.body_ = body;
        return *this;
    }
    
    HTTPRequest build() { return std::move(request_); }
    
private:
    HTTPRequest request_;
};

int main() {
    HTTPRequest request = HTTPRequestBuilder()
        .setMethod("POST")
        .setURL("/api/users")
        .addHeader("Content-Type", "application/json")
        .addHeader("Authorization", "Bearer token123")
        .setBody("{\"name\": \"John\", \"email\": \"john@example.com\"}")
        .build();
    
    std::cout << request.toString() << std::endl;
}

Problem: Implement a Builder pattern for creating documents in different formats.

Requirements:

  • Document class with title, content, author, and date fields
  • DocumentBuilder with fluent interface
  • Support optional footer and header
  • Method render() to display formatted document
Show Solution
#include <iostream>
#include <string>
#include <optional>

class Document {
public:
    void render() const {
        if (header_) std::cout << "[" << *header_ << "]" << std::endl;
        std::cout << "===============================" << std::endl;
        std::cout << "Title: " << title_ << std::endl;
        std::cout << "Author: " << author_ << std::endl;
        std::cout << "Date: " << date_ << std::endl;
        std::cout << "-------------------------------" << std::endl;
        std::cout << content_ << std::endl;
        std::cout << "===============================" << std::endl;
        if (footer_) std::cout << "[" << *footer_ << "]" << std::endl;
    }
    
private:
    std::string title_;
    std::string content_;
    std::string author_;
    std::string date_;
    std::optional<std::string> header_;
    std::optional<std::string> footer_;
    
    friend class DocumentBuilder;
};

class DocumentBuilder {
public:
    DocumentBuilder& setTitle(const std::string& title) {
        doc_.title_ = title;
        return *this;
    }
    
    DocumentBuilder& setContent(const std::string& content) {
        doc_.content_ = content;
        return *this;
    }
    
    DocumentBuilder& setAuthor(const std::string& author) {
        doc_.author_ = author;
        return *this;
    }
    
    DocumentBuilder& setDate(const std::string& date) {
        doc_.date_ = date;
        return *this;
    }
    
    DocumentBuilder& setHeader(const std::string& header) {
        doc_.header_ = header;
        return *this;
    }
    
    DocumentBuilder& setFooter(const std::string& footer) {
        doc_.footer_ = footer;
        return *this;
    }
    
    Document build() { return std::move(doc_); }
    
private:
    Document doc_;
};

int main() {
    Document report = DocumentBuilder()
        .setTitle("Q4 Report")
        .setAuthor("John Doe")
        .setDate("2024-12-15")
        .setContent("Sales increased by 25%...")
        .setHeader("CONFIDENTIAL")
        .setFooter("Page 1")
        .build();
    
    report.render();
}
02

Structural Patterns

Structural patterns deal with how classes and objects are composed to form larger structures. They help ensure that when one part of a system changes, the entire structure doesn't need to change.

The Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. Think of it like a power plug adapter when traveling - your laptop has one type of plug, but the wall outlet is different. The adapter sits in between and makes them compatible.

In software, you often need to use a class that has a different interface than what your code expects. Maybe you're integrating a third-party library, or working with legacy code. Instead of modifying the existing class (which might not be possible), you create an adapter that translates between interfaces.

#include <iostream>
#include <string>

// Legacy interface that we can't modify
class OldPrinter {
public:
    void printOldWay(const std::string& text) {
        std::cout << "*** OLD PRINTER ***" << std::endl;
        std::cout << text << std::endl;
        std::cout << "******************" << std::endl;
    }
};
This is the "adaptee" - an existing class with its own interface that we need to adapt. Perhaps it's from a third-party library you purchased, legacy code from years ago, or an external system you're integrating with. You can't modify OldPrinter's source code (it might be compiled, or changing it would break other systems). The printOldWay() method has a different signature and behavior than what your modern code expects. This is a common scenario when integrating disparate systems or maintaining backward compatibility while modernizing your codebase.
// New interface that our system expects
class Printer {
public:
    virtual ~Printer() = default;
    virtual void print(const std::string& document) = 0;
};
This is the "target" interface - the contract your modern, well-designed code expects to work with. Your entire codebase is built around this interface, with functions accepting Printer& parameters and calling the print() method. This interface represents your system's standard or convention. The goal of the Adapter pattern is to make the incompatible OldPrinter work through this interface without modifying either the OldPrinter class or rewriting all your existing code that depends on the Printer interface.
// Adapter - makes OldPrinter compatible with Printer interface
class PrinterAdapter : public Printer {
public:
    PrinterAdapter(OldPrinter& oldPrinter) : oldPrinter_(oldPrinter) {}
    
    void print(const std::string& document) override {
        // Translate the call to the old interface
        oldPrinter_.printOldWay(document);
    }
    
private:
    OldPrinter& oldPrinter_;
};
The PrinterAdapter is the "translator" that bridges the gap between incompatible interfaces. It inherits from the target interface (Printer) so it can be used anywhere a Printer is expected, but internally it holds a reference to an OldPrinter object. When someone calls print() on the adapter, it translates that call to printOldWay() on the wrapped OldPrinter. This is composition-based adaptation (the adapter "has-a" OldPrinter). The adapter pattern lets you reuse existing code without modification, following the Open/Closed Principle - your code is open for extension but closed for modification.
// Client code that expects the new Printer interface
void printDocument(Printer& printer, const std::string& doc) {
    printer.print(doc);
}

int main() {
    OldPrinter legacyPrinter;
    PrinterAdapter adapter(legacyPrinter);
    
    // Now we can use the old printer through the new interface
    printDocument(adapter, "Hello, Adapter Pattern!");
    
    return 0;
}
The client code demonstrates the adapter in action. The printDocument() function expects a Printer interface and knows nothing about OldPrinter or adapters - it simply calls print(). We create an OldPrinter instance and wrap it in a PrinterAdapter. Now we can pass the adapter to printDocument(), and it works seamlessly! The adapter pattern allows legacy code and modern code to coexist peacefully. This is especially valuable in large systems where you're gradually migrating from old APIs to new ones without breaking existing functionality.

The Decorator Pattern

The Decorator pattern lets you add new behaviors to objects dynamically by wrapping them in decorator objects. Think of it like adding toppings to a pizza - you start with a base pizza and can add cheese, pepperoni, mushrooms, etc. Each topping "decorates" the pizza with extra features.

This pattern follows the Open/Closed Principle - you can extend behavior without modifying existing code. Instead of creating a subclass for every combination of features, you compose decorators at runtime.

#include <iostream>
#include <string>
#include <memory>

// Component interface
class Coffee {
public:
    virtual ~Coffee() = default;
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
};

// Concrete component - base coffee
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override {
        return "Simple Coffee";
    }
    
    double getCost() const override {
        return 2.00;
    }
};
The SimpleCoffee class is the base component that implements the Coffee interface. This is the core object we'll be decorating - think of it as the plain pizza before adding toppings. It provides the fundamental behavior (returning a description and cost) without any extras. The Decorator pattern works by wrapping this base component in layers of decorators, each adding new functionality. SimpleCoffee remains unchanged and unaware of any decorators, following the Single Responsibility Principle - it only knows how to be a basic coffee.
// Base decorator - also implements Coffee interface
class CoffeeDecorator : public Coffee {
public:
    explicit CoffeeDecorator(std::unique_ptr<Coffee> coffee) 
        : coffee_(std::move(coffee)) {}
    
    std::string getDescription() const override {
        return coffee_->getDescription();
    }
    
    double getCost() const override {
        return coffee_->getCost();
    }
    
protected:
    std::unique_ptr<Coffee> coffee_;
};
CoffeeDecorator is the abstract base class for all decorators. It implements the Coffee interface (so decorators can be used anywhere a Coffee is expected) and holds a reference to another Coffee object (which could be SimpleCoffee or another decorator). By default, it simply delegates all calls to the wrapped coffee object without modification. Concrete decorators will inherit from this class and override methods to add their own behavior before or after the delegation. This "wrapping" approach using composition is key to the Decorator pattern - it provides flexibility to mix and match features at runtime without creating explosion of subclasses.
// Concrete decorators - add extras
class MilkDecorator : public CoffeeDecorator {
public:
    explicit MilkDecorator(std::unique_ptr<Coffee> coffee) 
        : CoffeeDecorator(std::move(coffee)) {}
    
    std::string getDescription() const override {
        return coffee_->getDescription() + ", Milk";
    }
    
    double getCost() const override {
        return coffee_->getCost() + 0.50;
    }
};

class SugarDecorator : public CoffeeDecorator {
public:
    explicit SugarDecorator(std::unique_ptr<Coffee> coffee) 
        : CoffeeDecorator(std::move(coffee)) {}
    
    std::string getDescription() const override {
        return coffee_->getDescription() + ", Sugar";
    }
    
    double getCost() const override {
        return coffee_->getCost() + 0.25;
    }
};

class WhippedCreamDecorator : public CoffeeDecorator {
public:
    explicit WhippedCreamDecorator(std::unique_ptr<Coffee> coffee) 
        : CoffeeDecorator(std::move(coffee)) {}
    
    std::string getDescription() const override {
        return coffee_->getDescription() + ", Whipped Cream";
    }
    
    double getCost() const override {
        return coffee_->getCost() + 0.75;
    }
};
Each concrete decorator adds a specific enhancement to the wrapped coffee. MilkDecorator appends ", Milk" to the description and adds $0.50 to the cost, while SugarDecorator and WhippedCreamDecorator do similarly for their respective additions. The beauty of this pattern is that decorators can be stacked infinitely - you can wrap a SimpleCoffee in MilkDecorator, then wrap that in SugarDecorator, then wrap that in WhippedCreamDecorator. Each decorator only knows about adding its own feature; it doesn't need to know what other decorators exist. This follows the Open/Closed Principle perfectly - you can add new decorator types (like CaramelDecorator or VanillaDecorator) without modifying any existing code.
int main() {
    // Start with simple coffee
    std::unique_ptr<Coffee> myCoffee = std::make_unique<SimpleCoffee>();
    std::cout << myCoffee->getDescription() << " - $" 
              << myCoffee->getCost() << std::endl;
    
    // Add milk
    myCoffee = std::make_unique<MilkDecorator>(std::move(myCoffee));
    std::cout << myCoffee->getDescription() << " - $" 
              << myCoffee->getCost() << std::endl;
    
    // Add sugar
    myCoffee = std::make_unique<SugarDecorator>(std::move(myCoffee));
    std::cout << myCoffee->getDescription() << " - $" 
              << myCoffee->getCost() << std::endl;
    
    // Add whipped cream
    myCoffee = std::make_unique<WhippedCreamDecorator>(std::move(myCoffee));
    std::cout << myCoffee->getDescription() << " - $" 
              << myCoffee->getCost() << std::endl;
    
    return 0;
}
// Output:
// Simple Coffee - $2
// Simple Coffee, Milk - $2.5
// Simple Coffee, Milk, Sugar - $2.75
// Simple Coffee, Milk, Sugar, Whipped Cream - $3.5
This example demonstrates dynamic decoration in action. We start with a simple coffee ($2.00) and progressively wrap it in decorators, each adding a feature and updating the price. Notice how we use std::move to transfer ownership when wrapping - each decorator takes ownership of the previous coffee object. The variable myCoffee always holds a Coffee pointer, whether it's pointing to SimpleCoffee or a complex chain of decorators. This runtime flexibility is the Decorator pattern's superpower - you can create any combination of features without needing a separate class for "CoffeeWithMilkAndSugarAndWhippedCream". The pattern allows unlimited combinations through composition rather than inheritance.

The Facade Pattern

The Facade pattern provides a simplified interface to a complex subsystem. Think of it like using a TV remote - you don't need to understand the complex electronics inside, you just press "power" and "volume up." The remote is a facade that hides the complexity.

Facades are useful when you have a complex system with many classes and you want to provide a simple, easy-to-use interface for common tasks. The client doesn't need to know about all the subsystem classes - they just interact with the facade.

#include <iostream>
#include <string>

// Complex subsystem classes
class CPU {
public:
    void freeze() { std::cout << "CPU: Freezing..." << std::endl; }
    void jump(long address) { 
        std::cout << "CPU: Jumping to " << address << std::endl; 
    }
    void execute() { std::cout << "CPU: Executing..." << std::endl; }
};

class Memory {
public:
    void load(long address, const std::string& data) {
        std::cout << "Memory: Loading '" << data 
                  << "' at " << address << std::endl;
    }
};

class HardDrive {
public:
    std::string read(long sector, int size) {
        std::cout << "HardDrive: Reading sector " << sector << std::endl;
        return "boot_data";
    }
};
These are the complex subsystem classes that handle low-level computer operations. Without a Facade, client code would need to understand all three classes (CPU, Memory, HardDrive) and know the exact sequence of method calls required to boot a computer: freeze the CPU, read boot data from the hard drive, load it into memory, make the CPU jump to the boot address, and execute. This complexity grows exponentially as you add more subsystem classes or operations. The client code becomes tightly coupled to the subsystem's internal structure, making it brittle and difficult to maintain when the subsystem changes.
// Facade - provides simple interface to complex subsystem
class ComputerFacade {
public:
    ComputerFacade() : cpu_(), memory_(), hardDrive_() {}
    
    void start() {
        std::cout << "=== Starting Computer ===" << std::endl;
        cpu_.freeze();
        std::string bootData = hardDrive_.read(0, 1024);
        memory_.load(0, bootData);
        cpu_.jump(0);
        cpu_.execute();
        std::cout << "=== Computer Started ===" << std::endl;
    }
    
    void shutdown() {
        std::cout << "=== Shutting Down ===" << std::endl;
        // Cleanup operations...
    }
    
private:
    CPU cpu_;
    Memory memory_;
    HardDrive hardDrive_;
};
The ComputerFacade encapsulates all the complexity of the subsystem behind two simple methods: start() and shutdown(). The facade internally manages the CPU, Memory, and HardDrive objects and orchestrates the correct sequence of operations. Client code no longer needs to know that booting requires freezing the CPU, reading from the hard drive, loading memory, jumping, and executing - they just call start(). This decouples clients from the subsystem's internal details. If the boot process changes (maybe you add a BIOS check or change the boot sequence), you only modify the facade, not every piece of client code. The Facade pattern reduces dependencies and provides a clean, high-level API for common operations.
ComputerFacade computer; // Simple interface - client doesn't need to know about CPU, Memory, etc. computer.start(); // Do some work... computer.shutdown(); return 0; }
The client code is remarkably simple - just create a ComputerFacade and call start() and shutdown(). Compare this to directly using the subsystem classes, where you'd need to manage CPU, Memory, and HardDrive objects and remember the exact calling sequence. The facade acts as a "front desk" that handles all the coordination behind the scenes. This makes the code more maintainable (changes to the subsystem don't affect clients), more testable (you can mock the facade instead of all subsystem classes), and more understandable (the high-level intent is clear). Facades are especially valuable when integrating third-party libraries or legacy systems with complex APIs - you create a simple facade that exposes only the functionality you need.

Practice Questions: Structural Patterns

Problem: You have a legacy CelsiusThermometer class but your new system expects a Thermometer interface with getTemperatureF().

Given:

class CelsiusThermometer {
public:
    double getTemperatureC() { return 25.0; }
};

Task: Create an adapter that makes CelsiusThermometer work with code expecting Fahrenheit.

Show Solution
class Thermometer {
public:
    virtual ~Thermometer() = default;
    virtual double getTemperatureF() = 0;
};

class ThermometerAdapter : public Thermometer {
public:
    ThermometerAdapter(CelsiusThermometer& t) : celsius_(t) {}
    
    double getTemperatureF() override {
        return celsius_.getTemperatureC() * 9.0 / 5.0 + 32.0;
    }
    
private:
    CelsiusThermometer& celsius_;
};

int main() {
    CelsiusThermometer oldThermo;
    ThermometerAdapter adapter(oldThermo);
    
    std::cout << adapter.getTemperatureF() << "°F" << std::endl;  // 77°F
}

Problem: Create a Pizza decorator system with base pizza and toppings.

Requirements:

  • Base Pizza interface with getDescription() and getPrice()
  • PlainPizza - $8.00
  • Decorators: Cheese (+$1.50), Pepperoni (+$2.00), Mushrooms (+$1.25)
Show Solution
#include <iostream>
#include <memory>

class Pizza {
public:
    virtual ~Pizza() = default;
    virtual std::string getDescription() const = 0;
    virtual double getPrice() const = 0;
};

class PlainPizza : public Pizza {
public:
    std::string getDescription() const override { return "Plain Pizza"; }
    double getPrice() const override { return 8.00; }
};

class ToppingDecorator : public Pizza {
public:
    ToppingDecorator(std::unique_ptr<Pizza> p) : pizza_(std::move(p)) {}
protected:
    std::unique_ptr<Pizza> pizza_;
};

class CheeseDecorator : public ToppingDecorator {
public:
    using ToppingDecorator::ToppingDecorator;
    std::string getDescription() const override {
        return pizza_->getDescription() + ", Extra Cheese";
    }
    double getPrice() const override { return pizza_->getPrice() + 1.50; }
};

class PepperoniDecorator : public ToppingDecorator {
public:
    using ToppingDecorator::ToppingDecorator;
    std::string getDescription() const override {
        return pizza_->getDescription() + ", Pepperoni";
    }
    double getPrice() const override { return pizza_->getPrice() + 2.00; }
};

int main() {
    std::unique_ptr<Pizza> pizza = std::make_unique<PlainPizza>();
    pizza = std::make_unique<CheeseDecorator>(std::move(pizza));
    pizza = std::make_unique<PepperoniDecorator>(std::move(pizza));
    
    std::cout << pizza->getDescription() << std::endl;
    std::cout << "$" << pizza->getPrice() << std::endl;
    // Plain Pizza, Extra Cheese, Pepperoni - $11.50
}

Problem: Create a Facade pattern for a complex home theater system.

Requirements:

  • Subsystem classes: TV, SoundSystem, StreamingPlayer, Lights
  • Each subsystem has on/off methods and specific operations
  • HomeTheaterFacade with watchMovie() and endMovie()
Show Solution
#include <iostream>
#include <string>

class TV {
public:
    void on() { std::cout << "TV: Turning on" << std::endl; }
    void off() { std::cout << "TV: Turning off" << std::endl; }
    void setInput(const std::string& input) {
        std::cout << "TV: Setting input to " << input << std::endl;
    }
};

class SoundSystem {
public:
    void on() { std::cout << "Sound: Powering on" << std::endl; }
    void off() { std::cout << "Sound: Powering off" << std::endl; }
    void setVolume(int level) {
        std::cout << "Sound: Volume set to " << level << std::endl;
    }
    void setSurroundMode() {
        std::cout << "Sound: Surround mode enabled" << std::endl;
    }
};

class StreamingPlayer {
public:
    void on() { std::cout << "Player: Starting up" << std::endl; }
    void off() { std::cout << "Player: Shutting down" << std::endl; }
    void play(const std::string& movie) {
        std::cout << "Player: Playing '" << movie << "'" << std::endl;
    }
    void stop() { std::cout << "Player: Stopped" << std::endl; }
};

class Lights {
public:
    void dim(int level) {
        std::cout << "Lights: Dimming to " << level << "%" << std::endl;
    }
    void on() { std::cout << "Lights: Full brightness" << std::endl; }
};

class HomeTheaterFacade {
public:
    void watchMovie(const std::string& movie) {
        std::cout << "=== Getting ready to watch " << movie << " ===" << std::endl;
        lights_.dim(10);
        tv_.on();
        tv_.setInput("HDMI1");
        sound_.on();
        sound_.setVolume(25);
        sound_.setSurroundMode();
        player_.on();
        player_.play(movie);
        std::cout << "=== Movie started! Enjoy! ===" << std::endl;
    }
    
    void endMovie() {
        std::cout << "=== Shutting down theater ===" << std::endl;
        player_.stop();
        player_.off();
        sound_.off();
        tv_.off();
        lights_.on();
        std::cout << "=== Theater shut down ===" << std::endl;
    }
    
private:
    TV tv_;
    SoundSystem sound_;
    StreamingPlayer player_;
    Lights lights_;
};

int main() {
    HomeTheaterFacade theater;
    theater.watchMovie("Inception");
    // ... watch movie ...
    theater.endMovie();
}

Problem: Adapt legacy audio players to work with a modern MediaPlayer interface.

Requirements:

  • Legacy classes: VLCPlayer with playVLC(), MP4Player with playMP4()
  • Modern interface: MediaPlayer with play(filename)
  • MediaAdapter that auto-detects format and uses appropriate player
Show Solution
#include <iostream>
#include <string>
#include <memory>

// Legacy players
class VLCPlayer {
public:
    void playVLC(const std::string& filename) {
        std::cout << "VLC: Playing " << filename << std::endl;
    }
};

class MP4Player {
public:
    void playMP4(const std::string& filename) {
        std::cout << "MP4 Player: Playing " << filename << std::endl;
    }
};

// Modern interface
class MediaPlayer {
public:
    virtual ~MediaPlayer() = default;
    virtual void play(const std::string& filename) = 0;
};

// Adapter
class MediaAdapter : public MediaPlayer {
public:
    void play(const std::string& filename) override {
        std::string ext = filename.substr(filename.find_last_of('.') + 1);
        
        if (ext == "vlc") {
            vlcPlayer_.playVLC(filename);
        } else if (ext == "mp4") {
            mp4Player_.playMP4(filename);
        } else {
            std::cout << "Unsupported format: " << ext << std::endl;
        }
    }
    
private:
    VLCPlayer vlcPlayer_;
    MP4Player mp4Player_;
};

// Client
class AudioPlayer : public MediaPlayer {
public:
    void play(const std::string& filename) override {
        std::string ext = filename.substr(filename.find_last_of('.') + 1);
        
        if (ext == "mp3") {
            std::cout << "AudioPlayer: Playing " << filename << std::endl;
        } else {
            adapter_.play(filename);
        }
    }
    
private:
    MediaAdapter adapter_;
};

int main() {
    AudioPlayer player;
    player.play("song.mp3");     // Native support
    player.play("video.mp4");    // Via adapter
    player.play("stream.vlc");   // Via adapter
}
03

Behavioral Patterns

Behavioral patterns are concerned with communication between objects, how they operate together, and how responsibilities are distributed among them.

The Observer Pattern

The Observer pattern defines a one-to-many dependency between objects. When one object (the "subject") changes state, all its dependents ("observers") are notified automatically. Think of it like a newsletter subscription - when a new article is published, all subscribers get notified.

This pattern is fundamental to event-driven programming. It's used extensively in GUI frameworks (button click handlers), reactive programming, and the Model-View-Controller (MVC) architecture.

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

// Observer interface
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& message) = 0;
};
The Observer interface is the contract that all observers must follow. It declares a pure virtual update() method that observers implement to receive notifications. This decouples the subject from concrete observer implementations - the subject only knows about the Observer interface, not specific observer types. The virtual destructor ensures proper cleanup when deleting observers through base class pointers. This interface-based design allows any class to become an observer by simply implementing the update method, making the pattern extremely flexible.
// Subject (Observable) class
class NewsAgency {
public:
    void subscribe(Observer* observer) {
        observers_.push_back(observer);
    }
The subscribe() method adds an observer to the notification list. The subject maintains a collection of observer pointers, allowing multiple observers to register interest in updates. This creates a one-to-many relationship where one subject can notify many observers. The observers list is dynamically managed - observers can be added or removed at runtime. This flexibility is key to the Observer pattern's power, enabling loose coupling between the subject and its observers.
    void unsubscribe(Observer* observer) {
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), observer),
            observers_.end()
        );
    }
The unsubscribe() method removes an observer from the notification list using the erase-remove idiom. This two-step process first uses std::remove to move the matching observer to the end, then erase to actually remove it from the vector. This allows observers to dynamically opt-out of notifications, giving them control over when they want to stop listening. Proper unsubscription is crucial for memory management and preventing notifications to destroyed observers.

    
    void publishNews(const std::string& news) {
        std::cout << "News Agency publishing: " << news << std::endl;
        notifyAll(news);
    }
The publishNews() method is the subject's state-changing operation that triggers notifications. When news is published, the subject doesn't directly notify observers - instead, it delegates to the private notifyAll() method. This separation of concerns keeps the public API clean and allows internal notification logic to be modified without affecting the public interface. The method represents the "change" in the observer pattern that causes all registered observers to be updated.
private:
    void notifyAll(const std::string& message) {
        for (Observer* observer : observers_) {
            observer->update(message);
        }
    }
    
    std::vector<Observer*> observers_;
};
The private notifyAll() method iterates through all registered observers and calls their update() methods, passing the news message. This is where the "push" notification happens - the subject actively pushes data to all observers. The method doesn't care what each observer does with the notification; it simply ensures everyone gets informed. The observers vector stores raw pointers because the subject doesn't own the observers - they have independent lifetimes managed elsewhere.
// Concrete observers
class NewsChannel : public Observer {
public:
    NewsChannel(const std::string& name) : name_(name) {}
    
    void update(const std::string& message) override {
        std::cout << name_ << " received: " << message << std::endl;
    }
    
private:
    std::string name_;
};
The NewsChannel class is a concrete observer that receives news updates. It implements the update() method by printing the received message to simulate broadcasting on television. Each observer can have its own state (like the channel name) and react to updates in its own way. The key is that the NewsChannel doesn't need to know anything about the NewsAgency's internal structure - it only needs to implement the Observer interface. This decoupling allows you to add new observer types without modifying the subject.
class NewsApp : public Observer {
public:
    NewsApp(const std::string& name) : name_(name) {}
    
    void update(const std::string& message) override {
        std::cout << name_ << " notification: " << message << std::endl;
    }
    
private:
    std::string name_;
};
The NewsApp class demonstrates the Observer pattern's flexibility - it implements the same interface as NewsChannel but with different behavior (sending push notifications instead of broadcasting). Multiple observer types can coexist, each handling notifications in their own way. The subject (NewsAgency) treats all observers uniformly through the Observer interface, regardless of their concrete types. This polymorphic approach is what makes the pattern so powerful for event-driven systems.
int main() {
    NewsAgency agency;
    
    NewsChannel cnn("CNN");
    NewsChannel bbc("BBC");
    NewsApp mobileApp("NewsApp");
    
    // Subscribe to news
    agency.subscribe(&cnn);
    agency.subscribe(&bbc);
    agency.subscribe(&mobileApp);
    
    // Publish news - all observers notified
    agency.publishNews("Breaking: C++ 26 features announced!");
    
    std::cout << "\n--- BBC unsubscribes ---\n" << std::endl;
    agency.unsubscribe(&bbc);
    
    // Only CNN and NewsApp receive this
    agency.publishNews("Tech stocks rally on AI news");
    
    return 0;
}
// Output:
// News Agency publishing: Breaking: C++ 26 features announced!
// CNN received: Breaking: C++ 26 features announced!
// BBC received: Breaking: C++ 26 features announced!
// NewsApp notification: Breaking: C++ 26 features announced!
//
// --- BBC unsubscribes ---
//
// News Agency publishing: Tech stocks rally on AI news
// CNN received: Tech stocks rally on AI news
// NewsApp notification: Tech stocks rally on AI news
This example demonstrates the Observer pattern's core benefit: automatic notification propagation. When publishNews() is called, all subscribed observers receive updates without the NewsAgency needing to know their concrete types. The output shows that CNN, BBC, and NewsApp all receive the first announcement. After BBC unsubscribes, only CNN and NewsApp receive the second announcement, demonstrating dynamic subscription management. This pattern is foundational for GUI event handling, reactive programming frameworks, and Model-View-Controller architectures where views automatically update when the model changes.

The Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from the clients that use it. Think of it like choosing a payment method at checkout - you can pay with credit card, PayPal, or cash. Each is a different "strategy" for payment.

This pattern is great when you have multiple ways to perform an operation and want to switch between them at runtime. It follows the Open/Closed Principle - you can add new strategies without modifying existing code.

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>

// Strategy interface
class SortStrategy {
public:
    virtual ~SortStrategy() = default;
    virtual void sort(std::vector<int>& data) = 0;
    virtual std::string getName() const = 0;
};
The SortStrategy interface defines the contract for all sorting algorithms. Each strategy must implement sort() to perform the algorithm and getName() for identification. This abstraction allows the context class to work with any sorting algorithm without knowing implementation details. The interface follows the Dependency Inversion Principle - high-level code depends on the abstraction, not concrete implementations. Adding new sorting algorithms requires only creating a new class that implements this interface, without modifying existing code.
// Concrete strategies
class BubbleSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        for (size_t i = 0; i < data.size(); ++i) {
            for (size_t j = 0; j < data.size() - i - 1; ++j) {
                if (data[j] > data[j + 1]) {
                    std::swap(data[j], data[j + 1]);
                }
            }
        }
    }
    
    std::string getName() const override { return "Bubble Sort"; }
};
BubbleSort is a concrete strategy implementing the SortStrategy interface with a simple O(n²) sorting algorithm. It repeatedly steps through the list, compares adjacent elements, and swaps them if they're in the wrong order. While not efficient for large datasets, it's simple to understand and demonstrates how each strategy encapsulates a complete algorithm. The strategy pattern allows this implementation to be swapped with more efficient algorithms at runtime without changing client code. Each strategy is self-contained with its own logic.


class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        quickSort(data, 0, data.size() - 1);
    }
    
    std::string getName() const override { return "Quick Sort"; }
    
private:
    void quickSort(std::vector<int>& arr, int low, int high) {
        if (low < high) {
            int pivot = partition(arr, low, high);
            quickSort(arr, low, pivot - 1);
            quickSort(arr, pivot + 1, high);
        }
    }
    
    int partition(std::vector<int>& arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;
        for (int j = low; j < high; ++j) {
            if (arr[j] <= pivot) {
                ++i;
                std::swap(arr[i], arr[j]);
            }
        }
        std::swap(arr[i + 1], arr[high]);
        return i + 1;
    }
};
QuickSort demonstrates a more complex strategy with O(n log n) average performance. It uses divide-and-conquer, partitioning the array around a pivot and recursively sorting sub-arrays. The strategy encapsulates all the complexity - helper methods like partition() and recursive quickSort() - as private implementation details. The context class doesn't need to know about these internals; it only calls the public sort() method. This encapsulation is a key benefit of the Strategy pattern - each algorithm is self-contained and can be as simple or complex as needed.


class STLSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        std::sort(data.begin(), data.end());
    }
    
    std::string getName() const override { return "STL Sort"; }
};
STLSort wraps the standard library's highly-optimized sort algorithm (typically introsort - a hybrid of quicksort, heapsort, and insertion sort). This demonstrates the Strategy pattern's power: you can leverage existing implementations by simply wrapping them in the strategy interface. Each strategy is independent - you can add, remove, or modify strategies without affecting others or the context class. This follows the Open/Closed Principle perfectly: the system is open for extension (add new strategies) but closed for modification (no need to change existing code).
// Context class that uses the strategy
class Sorter {
public:
    void setStrategy(std::unique_ptr<SortStrategy> strategy) {
        strategy_ = std::move(strategy);
    }
    
    void performSort(std::vector<int>& data) {
        if (strategy_) {
            std::cout << "Using " << strategy_->getName() << std::endl;
            strategy_->sort(data);
        }
    }
    
private:
    std::unique_ptr<SortStrategy> strategy_;
};
The Sorter class is the context that uses strategies. It holds a strategy object and delegates sorting to it through performSort(). The context doesn't implement sorting logic itself - it relies entirely on the injected strategy. The setStrategy() method allows runtime strategy switching, which is the pattern's key feature. The context uses std::unique_ptr for automatic memory management, taking ownership of the strategy. This design separates the algorithm selection (context's responsibility) from algorithm implementation (strategy's responsibility).
void printVector(const std::vector<int>& v) {
    for (int n : v) std::cout << n << " ";
    std::cout << std::endl;
}

int main() {
    Sorter sorter;
    std::vector<int> data = {64, 34, 25, 12, 22, 11, 90};
    
    std::cout << "Original: ";
    printVector(data);
    
    // Use Bubble Sort
    sorter.setStrategy(std::make_unique<BubbleSort>());
    auto copy1 = data;
    sorter.performSort(copy1);
    std::cout << "Result: ";
    printVector(copy1);
    
    // Switch to Quick Sort at runtime
    sorter.setStrategy(std::make_unique<QuickSort>());
    auto copy2 = data;
    sorter.performSort(copy2);
    std::cout << "Result: ";
    printVector(copy2);
    
    return 0;
}
This example demonstrates runtime strategy switching - the same Sorter object uses different algorithms by calling setStrategy(). The client code remains clean and doesn't contain any sorting logic. You can easily add new strategies (like MergeSort or HeapSort) without modifying the Sorter class or main function. This pattern is widely used in game AI (different enemy behaviors), payment processing (different payment methods), data validation (different validation rules), and compression (different compression algorithms). The strategy pattern provides algorithmic flexibility with clean separation of concerns.

The Command Pattern

The Command pattern encapsulates a request as an object, letting you parameterize clients with different requests, queue requests, and support undoable operations. Think of it like a restaurant order ticket - the waiter writes down your order (command), passes it to the kitchen (invoker), which executes it. The order can be modified, cancelled, or kept for records.

This pattern is essential for implementing undo/redo functionality, transaction systems, macro recording, and command queuing. Each command knows how to execute itself and optionally how to undo itself.

#include <iostream>
#include <memory>
#include <vector>
#include <stack>
#include <string>

// Receiver - the actual object that performs the work
class TextEditor {
public:
    void insertText(const std::string& text, size_t position) {
        content_.insert(position, text);
    }
    
    void deleteText(size_t position, size_t length) {
        content_.erase(position, length);
    }
    
    std::string getContent() const { return content_; }
    size_t getLength() const { return content_.length(); }
    
private:
    std::string content_;
};
The TextEditor is the "receiver" in the Command pattern - the object that performs the actual work. It provides methods like insertText() and deleteText() that manipulate the document content. The receiver doesn't know anything about commands; it just provides operations. This separation is key to the Command pattern: the receiver focuses on business logic (text editing), while commands handle invocation, queuing, and undo/redo. Multiple commands can call the same receiver methods in different combinations to create complex operations.
// Command interface
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
};
The Command interface declares methods for execution and undo. Every command must implement both execute() (to perform the action) and undo() (to reverse it). This interface is the core of the pattern - it allows treating all commands uniformly, regardless of what they do. Commands are first-class objects that encapsulate a request as a self-contained object. This enables powerful features like command history, macro recording, transaction rollback, and distributed command execution. The pattern decouples the sender (who triggers the command) from the receiver (who performs the work).
// Concrete commands
class InsertCommand : public Command {
public:
    InsertCommand(TextEditor& editor, const std::string& text, size_t pos)
        : editor_(editor), text_(text), position_(pos) {}
    
    void execute() override {
        editor_.insertText(text_, position_);
    }
    
    void undo() override {
        editor_.deleteText(position_, text_.length());
    }
    
private:
    TextEditor& editor_;
    std::string text_;
    size_t position_;
};
InsertCommand encapsulates a text insertion operation. It stores the receiver reference, the text to insert, and the position - all data needed for both execution and undo. The execute() method calls the receiver's insertText(), while undo() reverses it by deleting the inserted text. Notice how the command stores all necessary state - this makes it self-contained and reusable. You could store these command objects in a queue, execute them later, replay them, or send them across a network. This is the command pattern's power: turning method calls into objects.


class DeleteCommand : public Command {
public:
    DeleteCommand(TextEditor& editor, size_t pos, size_t len)
        : editor_(editor), position_(pos), length_(len) {
        // Save deleted text for undo
        deletedText_ = editor_.getContent().substr(pos, len);
    }
    
    void execute() override {
        editor_.deleteText(position_, length_);
    }
    
    void undo() override {
        editor_.insertText(deletedText_, position_);
    }
    
private:
    TextEditor& editor_;
    size_t position_;
    size_t length_;
    std::string deletedText_;
};
DeleteCommand demonstrates a crucial Command pattern feature: storing state for undo. The constructor saves the text being deleted by reading it from the editor before deletion. This saved text is used later to restore the content during undo. Each command is responsible for storing whatever state it needs to reverse itself. The execute method deletes the text, while undo re-inserts the saved text at the original position. This pattern makes implementing complex undo/redo systems straightforward - each command handles its own undo logic independently.
// Invoker - manages command execution and history
class CommandManager {
public:
    void executeCommand(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history_.push(std::move(cmd));
        // Clear redo stack when new command is executed
        while (!redoStack_.empty()) redoStack_.pop();
    }
The CommandManager is the "invoker" that executes commands and maintains history. The executeCommand() method calls the command's execute() and pushes it onto the history stack for potential undo. Crucially, it clears the redo stack when a new command is executed - this is standard undo/redo behavior (after typing new text, you can't redo the text you previously deleted). The invoker doesn't know what commands do; it only knows they have execute() and undo() methods. This separation allows adding new command types without modifying the invoker.
    void undo() {
        if (!history_.empty()) {
            auto cmd = std::move(history_.top());
            history_.pop();
            cmd->undo();
            redoStack_.push(std::move(cmd));
        }
    }
The undo() method pops the most recent command from history, calls its undo() method, and moves it to the redo stack. This allows undoing the last operation and potentially redoing it later. The method uses std::unique_ptr for automatic memory management - when a command is moved from one stack to another, ownership transfers cleanly. The pattern elegantly handles arbitrary undo depth - the history stack can hold any number of commands, limited only by memory.
    void redo() {
        if (!redoStack_.empty()) {
            auto cmd = std::move(redoStack_.top());
            redoStack_.pop();
            cmd->execute();
            history_.push(std::move(cmd));
        }
    }
    
private:
    std::stack<std::unique_ptr<Command>> history_;
    std::stack<std::unique_ptr<Command>> redoStack_;
};
The redo() method reverses an undo by popping from the redo stack, executing the command again, and pushing it back to history. This creates the classic undo/redo cycle found in text editors and other applications. The two stacks (history and redo) work together to enable bidirectional navigation through command history. The Command pattern makes implementing this feature straightforward - without it, you'd need complex state tracking for every possible operation. Commands encapsulate all the logic for both doing and undoing actions.
int main() {
    TextEditor editor;
    CommandManager manager;
    
    // Execute commands
    manager.executeCommand(
        std::make_unique<InsertCommand>(editor, "Hello", 0));
    std::cout << "After insert: " << editor.getContent() << std::endl;
    
    manager.executeCommand(
        std::make_unique<InsertCommand>(editor, " World", 5));
    std::cout << "After insert: " << editor.getContent() << std::endl;
    
    // Undo
    manager.undo();
    std::cout << "After undo: " << editor.getContent() << std::endl;
    
    // Redo
    manager.redo();
    std::cout << "After redo: " << editor.getContent() << std::endl;
    
    return 0;
}
// Output:
// After insert: Hello
// After insert: Hello World
// After undo: Hello
// After redo: Hello World
This example demonstrates the Command pattern's undo/redo capability. Commands are executed through the CommandManager, which automatically tracks them in history. Calling undo() reverses the last operation (removing " World"), while redo() reapplies it. The pattern's power lies in its flexibility: you could add a "save all commands to file" feature for macro recording, implement multi-level undo (just keep more history), add command validation before execution, queue commands for batch processing, or implement transactional behavior where a group of commands can be rolled back as a unit. The Command pattern is fundamental to many professional applications.

Practice Questions: Behavioral Patterns

Problem: Create a WeatherStation (subject) that notifies displays (observers) when temperature changes.

Requirements:

  • WeatherStation with setTemperature(double)
  • Display observers that print "Temperature updated to X°C"
  • Support multiple displays
Show Solution
#include <iostream>
#include <vector>
#include <string>

class WeatherObserver {
public:
    virtual void update(double temp) = 0;
    virtual ~WeatherObserver() = default;
};

class WeatherStation {
public:
    void subscribe(WeatherObserver* obs) { observers_.push_back(obs); }
    
    void setTemperature(double temp) {
        temperature_ = temp;
        for (auto* obs : observers_) {
            obs->update(temp);
        }
    }
    
private:
    double temperature_ = 0;
    std::vector<WeatherObserver*> observers_;
};

class Display : public WeatherObserver {
public:
    Display(const std::string& name) : name_(name) {}
    
    void update(double temp) override {
        std::cout << name_ << ": Temperature updated to " 
                  << temp << "°C" << std::endl;
    }
    
private:
    std::string name_;
};

int main() {
    WeatherStation station;
    Display phone("Phone");
    Display tv("TV");
    
    station.subscribe(&phone);
    station.subscribe(&tv);
    
    station.setTemperature(25.5);
    // Phone: Temperature updated to 25.5°C
    // TV: Temperature updated to 25.5°C
}

Problem: Implement payment processing with different strategies.

Requirements:

  • PaymentStrategy interface with pay(double amount)
  • CreditCard, PayPal, and Crypto payment strategies
  • ShoppingCart that uses the selected strategy
Show Solution
#include <iostream>
#include <memory>

class PaymentStrategy {
public:
    virtual ~PaymentStrategy() = default;
    virtual void pay(double amount) = 0;
};

class CreditCard : public PaymentStrategy {
public:
    CreditCard(const std::string& num) : cardNumber_(num) {}
    void pay(double amount) override {
        std::cout << "Paid $" << amount << " with Credit Card "
                  << cardNumber_.substr(cardNumber_.length()-4) << std::endl;
    }
private:
    std::string cardNumber_;
};

class PayPal : public PaymentStrategy {
public:
    PayPal(const std::string& email) : email_(email) {}
    void pay(double amount) override {
        std::cout << "Paid $" << amount << " via PayPal (" 
                  << email_ << ")" << std::endl;
    }
private:
    std::string email_;
};

class ShoppingCart {
public:
    void setPaymentMethod(std::unique_ptr<PaymentStrategy> method) {
        payment_ = std::move(method);
    }
    
    void checkout(double total) {
        if (payment_) payment_->pay(total);
    }
    
private:
    std::unique_ptr<PaymentStrategy> payment_;
};

int main() {
    ShoppingCart cart;
    
    cart.setPaymentMethod(std::make_unique<CreditCard>("1234567890123456"));
    cart.checkout(99.99);  // Paid $99.99 with Credit Card 3456
    
    cart.setPaymentMethod(std::make_unique<PayPal>("user@email.com"));
    cart.checkout(49.99);  // Paid $49.99 via PayPal
}

Problem: Create a calculator that supports undo/redo operations.

Requirements:

  • Calculator with current value
  • Commands: Add, Subtract, Multiply, Divide
  • Support undo() and redo() operations
  • Each command stores the operand for undo
Show Solution
#include <iostream>
#include <stack>
#include <memory>

class Calculator {
public:
    double getValue() const { return value_; }
    void setValue(double v) { value_ = v; }
private:
    double value_ = 0;
};

class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
};

class AddCommand : public Command {
public:
    AddCommand(Calculator& calc, double value) 
        : calc_(calc), value_(value) {}
    
    void execute() override { calc_.setValue(calc_.getValue() + value_); }
    void undo() override { calc_.setValue(calc_.getValue() - value_); }
    
private:
    Calculator& calc_;
    double value_;
};

class SubtractCommand : public Command {
public:
    SubtractCommand(Calculator& calc, double value) 
        : calc_(calc), value_(value) {}
    
    void execute() override { calc_.setValue(calc_.getValue() - value_); }
    void undo() override { calc_.setValue(calc_.getValue() + value_); }
    
private:
    Calculator& calc_;
    double value_;
};

class MultiplyCommand : public Command {
public:
    MultiplyCommand(Calculator& calc, double value) 
        : calc_(calc), value_(value) {}
    
    void execute() override { calc_.setValue(calc_.getValue() * value_); }
    void undo() override { calc_.setValue(calc_.getValue() / value_); }
    
private:
    Calculator& calc_;
    double value_;
};

class CalculatorInvoker {
public:
    void executeCommand(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history_.push(std::move(cmd));
        while (!redoStack_.empty()) redoStack_.pop();
    }
    
    void undo() {
        if (history_.empty()) return;
        auto cmd = std::move(history_.top());
        history_.pop();
        cmd->undo();
        redoStack_.push(std::move(cmd));
    }
    
    void redo() {
        if (redoStack_.empty()) return;
        auto cmd = std::move(redoStack_.top());
        redoStack_.pop();
        cmd->execute();
        history_.push(std::move(cmd));
    }
    
private:
    std::stack<std::unique_ptr<Command>> history_;
    std::stack<std::unique_ptr<Command>> redoStack_;
};

int main() {
    Calculator calc;
    CalculatorInvoker invoker;
    
    invoker.executeCommand(std::make_unique<AddCommand>(calc, 10));
    std::cout << "After +10: " << calc.getValue() << std::endl;  // 10
    
    invoker.executeCommand(std::make_unique<MultiplyCommand>(calc, 5));
    std::cout << "After *5: " << calc.getValue() << std::endl;   // 50
    
    invoker.undo();
    std::cout << "After undo: " << calc.getValue() << std::endl; // 10
    
    invoker.redo();
    std::cout << "After redo: " << calc.getValue() << std::endl; // 50
}

Problem: Implement a stock price notification system using Observer pattern.

Requirements:

  • Stock class with symbol and price
  • Investors as observers who get notified of price changes
  • Support subscribe(), unsubscribe(), and notify()
  • Investors should see "Stock X changed from Y to Z"
Show Solution
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

class StockObserver {
public:
    virtual ~StockObserver() = default;
    virtual void update(const std::string& symbol, 
                       double oldPrice, double newPrice) = 0;
};

class Stock {
public:
    Stock(const std::string& symbol, double price) 
        : symbol_(symbol), price_(price) {}
    
    void subscribe(StockObserver* obs) {
        observers_.push_back(obs);
    }
    
    void unsubscribe(StockObserver* obs) {
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), obs),
            observers_.end());
    }
    
    void setPrice(double newPrice) {
        double oldPrice = price_;
        price_ = newPrice;
        notify(oldPrice, newPrice);
    }
    
    double getPrice() const { return price_; }
    std::string getSymbol() const { return symbol_; }
    
private:
    void notify(double oldPrice, double newPrice) {
        for (auto* obs : observers_) {
            obs->update(symbol_, oldPrice, newPrice);
        }
    }
    
    std::string symbol_;
    double price_;
    std::vector<StockObserver*> observers_;
};

class Investor : public StockObserver {
public:
    Investor(const std::string& name) : name_(name) {}
    
    void update(const std::string& symbol, 
               double oldPrice, double newPrice) override {
        std::cout << name_ << ": Stock " << symbol 
                  << " changed from $" << oldPrice 
                  << " to $" << newPrice;
        if (newPrice > oldPrice) {
            std::cout << " (+" << (newPrice - oldPrice) << ")";
        } else {
            std::cout << " (" << (newPrice - oldPrice) << ")";
        }
        std::cout << std::endl;
    }
    
private:
    std::string name_;
};

int main() {
    Stock apple("AAPL", 150.0);
    
    Investor john("John");
    Investor jane("Jane");
    
    apple.subscribe(&john);
    apple.subscribe(&jane);
    
    apple.setPrice(155.0);  // Both notified
    apple.setPrice(148.0);  // Both notified
    
    apple.unsubscribe(&jane);
    apple.setPrice(160.0);  // Only John notified
}
04

Modern C++ Idioms

Modern C++ introduces powerful idioms that go beyond classic GoF patterns. These idioms leverage C++'s unique features like templates, RAII, and compile-time programming.

RAII - Resource Acquisition Is Initialization

RAII is perhaps the most important C++ idiom. The idea is simple: tie the lifecycle of a resource (memory, file handle, mutex lock, network connection) to the lifetime of an object. When the object is created, it acquires the resource. When the object is destroyed, it releases the resource.

This pattern eliminates resource leaks because C++ guarantees that destructors are called when objects go out of scope, even when exceptions are thrown. Smart pointers (unique_ptr, shared_ptr) are the most common examples of RAII in modern C++.

#include <iostream>
#include <fstream>
#include <mutex>
#include <memory>

// Custom RAII wrapper for file handling
class FileHandle {
public:
    explicit FileHandle(const std::string& filename) 
        : file_(filename) {
        if (!file_.is_open()) {
            throw std::runtime_error("Failed to open file: " + filename);
        }
        std::cout << "File opened: " << filename << std::endl;
    }
The FileHandle constructor implements the "Acquisition" part of RAII - it acquires the file resource by opening it. If the file can't be opened, it throws an exception, preventing construction of an invalid object. This follows the principle that a successfully constructed object should always be in a valid state. The constructor explicitly marks resource acquisition, making it clear when the resource lifetime begins. RAII ties resource management to object lifetime, eliminating manual cleanup and ensuring exception safety.
    ~FileHandle() {
        if (file_.is_open()) {
            file_.close();
            std::cout << "File closed automatically" << std::endl;
        }
    }
The destructor implements the "Release" part of RAII - it automatically closes the file when the object is destroyed. C++ guarantees destructors are called when objects go out of scope, even if exceptions are thrown. This means you can never forget to close the file - it happens automatically. No try/finally blocks needed, no manual cleanup, no resource leaks. The destructor runs deterministically at the end of the scope, making resource management predictable and safe. This is the core power of RAII.
    // Delete copy operations
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // Allow move operations
    FileHandle(FileHandle&& other) noexcept : file_(std::move(other.file_)) {}
    
    void write(const std::string& data) {
        file_ << data;
    }
    
private:
    std::ofstream file_;
};
The class deletes copy operations because you can't have two FileHandle objects managing the same file - that would lead to double-close errors. However, move operations are allowed, enabling transfer of ownership from one FileHandle to another (useful for returning from functions). The write() method provides the public interface for using the resource. The private file_ member is the actual resource being managed. This design enforces unique ownership semantics - exactly one FileHandle owns the file at any time, preventing resource management conflicts.
// RAII mutex lock
class LockGuard {
public:
    explicit LockGuard(std::mutex& mtx) : mutex_(mtx) {
        mutex_.lock();
        std::cout << "Mutex locked" << std::endl;
    }
The LockGuard constructor acquires the mutex lock, making the critical section thread-safe from the moment the object is created. This implements RAII for synchronization primitives. The lock acquisition happens automatically when LockGuard is constructed, so you can't forget to lock before entering a critical section. The explicit constructor prevents accidental implicit conversions. This simple design eliminates a whole class of concurrency bugs where programmers forget to acquire locks before accessing shared data.
    ~LockGuard() {
        mutex_.unlock();
        std::cout << "Mutex unlocked" << std::endl;
    }
    
    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;
    
private:
    std::mutex& mutex_;
};
The destructor automatically releases the mutex when the LockGuard goes out of scope, ensuring the lock is always released even if exceptions are thrown. This prevents deadlocks caused by forgetting to unlock. The deleted copy operations prevent multiple LockGuard objects from trying to unlock the same mutex. This is essentially what std::lock_guard does in the standard library. The pattern guarantees exception-safe locking - if your code throws an exception while holding the lock, the destructor still runs and releases it.
void processWithLock(std::mutex& mtx) {
    LockGuard lock(mtx);  // Automatically locks
    
    // Do work...
    std::cout << "Processing..." << std::endl;
    
    // If exception thrown here, destructor still runs!
    // throw std::runtime_error("Error!");
    
}  // Automatically unlocks when lock goes out of scope
This usage example demonstrates the elegance of RAII for mutex management. The lock is acquired when lock is constructed and released when it goes out of scope at the closing brace. Even if an exception is thrown in the middle of processing, the destructor runs and unlocks the mutex. No manual unlock calls needed, no try/finally blocks, no possibility of forgetting to unlock. The scope-based lifetime makes the critical section boundaries visually clear. This pattern is fundamental to writing exception-safe multithreaded code in C++.
int main() {
    // Smart pointers are RAII for dynamic memory
    {
        auto ptr = std::make_unique<int>(42);
        std::cout << "Value: " << *ptr << std::endl;
    }  // Memory automatically freed here
    
    // File RAII
    try {
        FileHandle file("output.txt");
        file.write("Hello, RAII!\n");
        // File automatically closed when 'file' goes out of scope
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    
    // Mutex RAII
    std::mutex mtx;
    processWithLock(mtx);
    
    return 0;
}
This example shows three RAII use cases: smart pointers for memory, FileHandle for files, and LockGuard for mutexes. Each demonstrates automatic resource cleanup. The smart pointer's memory is freed when it goes out of scope (the closing brace). The file is closed even if an exception occurs. The mutex is unlocked automatically. RAII eliminates manual cleanup, prevents resource leaks, and makes code exception-safe. This idiom is so fundamental to C++ that the standard library provides RAII wrappers for most resources: std::unique_ptr, std::lock_guard, std::fstream, etc.

CRTP - Curiously Recurring Template Pattern

CRTP is a template technique where a class inherits from a template instantiated with itself. It sounds mind-bending, but it's incredibly powerful for adding functionality to classes at compile time without the overhead of virtual functions.

Common uses include: implementing the Singleton pattern, adding comparison operators, implementing object counting, and creating compile-time polymorphism (static polymorphism).

#include <iostream>

// CRTP base - provides functionality to derived classes
template<typename Derived>
class Counter {
public:
    Counter() { ++count_; }
    Counter(const Counter&) { ++count_; }
    ~Counter() { --count_; }
    
    static int getCount() { return count_; }
    
private:
    static int count_;
};

template<typename Derived>
int Counter<Derived>::count_ = 0;
The Counter template uses CRTP to provide instance counting to any derived class. The key is the template parameter Derived - each different type that inherits from Counter gets its own static counter variable. When you write Counter<Dog> and Counter<Cat>, the compiler generates two completely separate Counter classes with separate static count_ variables. The constructors increment the count, the destructor decrements it, tracking how many instances of each specific type exist. This compile-time polymorphism has zero runtime overhead compared to virtual functions.
// Classes using CRTP - each gets its own counter!
class Dog : public Counter<Dog> {
public:
    Dog(const std::string& name) : name_(name) {}
    std::string name_;
};

class Cat : public Counter<Cat> {
public:
    Cat(const std::string& name) : name_(name) {}
    std::string name_;
};

int main() {
    Dog d1("Rex"), d2("Buddy"), d3("Max");
    Cat c1("Whiskers"), c2("Mittens");
    
    std::cout << "Dogs: " << Dog::getCount() << std::endl;  // 3
    std::cout << "Cats: " << Cat::getCount() << std::endl;  // 2
    
    {
        Dog temp("Temp");
        std::cout << "Dogs (with temp): " << Dog::getCount() << std::endl;  // 4
    }
    
    std::cout << "Dogs (after temp): " << Dog::getCount() << std::endl;  // 3
    
    return 0;
}
This example demonstrates CRTP's power: Dog and Cat each get independent counters by inheriting from Counter<Dog> and Counter<Cat> respectively. The counters track object lifetimes automatically - when objects are created, counts increase; when they're destroyed (like temp going out of scope), counts decrease. Each class has its own separate counter maintained by the template system. CRTP provides this functionality without runtime polymorphism overhead - no virtual functions, no vtable lookups, everything resolved at compile time. This pattern is used throughout the C++ standard library for policies and mixins.
// CRTP for static polymorphism - no virtual function overhead!
template<typename Derived>
class Shape {
public:
    void draw() {
        // Call derived class implementation without virtual
        static_cast<Derived*>(this)->drawImpl();
    }
    
    double area() {
        return static_cast<Derived*>(this)->areaImpl();
    }
};
This Shape template demonstrates "static polymorphism" - achieving polymorphic behavior without virtual functions. The base class methods use static_cast<Derived*>(this) to call derived class implementations. This cast is safe because the template guarantees Derived actually inherits from Shape<Derived>. The compiler knows the exact type at compile time, so it can inline the calls and optimize aggressively - no vtable lookup, no indirection. This gives you the flexibility of polymorphism with the performance of direct function calls. It's called "curiously recurring" because the class inherits from a template instantiated with itself.
class Circle : public Shape<Circle> {
public:
    Circle(double r) : radius_(r) {}
    
    void drawImpl() {
        std::cout << "Drawing Circle with radius " << radius_ << std::endl;
    }
    
    double areaImpl() {
        return 3.14159 * radius_ * radius_;
    }
    
private:
    double radius_;
};
Circle inherits from Shape<Circle> (the curiously recurring part) and provides drawImpl() and areaImpl() implementations. When you call circle.draw(), the Shape template's draw() method casts this to Circle* and calls drawImpl(). The compiler sees the exact type (Circle) at compile time and can inline the entire call chain. This achieves code reuse (Shape provides the interface) with zero runtime cost. Compare this to virtual functions which require runtime type identification and vtable lookups.
class Square : public Shape<Square> {
public:
    Square(double s) : side_(s) {}
    
    void drawImpl() {
        std::cout << "Drawing Square with side " << side_ << std::endl;
    }
    
    double areaImpl() {
        return side_ * side_;
    }
    
private:
    double side_;
};
Square follows the same pattern, inheriting from Shape<Square> and providing its own implementations. Each shape gets its own specialized version of the Shape template. The beauty of CRTP is that Circle and Square are completely independent - they don't share a vtable, and you can't store them in the same container polymorphically (they're different types). This is a trade-off: you lose runtime polymorphism but gain performance and enable compile-time optimizations. CRTP is ideal when you know all types at compile time and need maximum performance.

Pimpl - Pointer to Implementation

The Pimpl (Pointer to Implementation) idiom hides implementation details from the header file. Only a forward declaration of the implementation class appears in the header - all the actual implementation is in the source file. This reduces compilation dependencies and compile times.

When you change implementation details (private members), only the .cpp file needs recompilation - not all the files that include the header. This is especially valuable in large projects.

// Widget.h - Header file (minimal dependencies)
#ifndef WIDGET_H
#define WIDGET_H

#include <memory>
#include <string>

class Widget {
public:
    Widget(const std::string& name);
    ~Widget();  // Must be declared for unique_ptr with incomplete type
The Widget header contains only minimal includes (<memory> and <string>) and forward-declares the Impl class. This is the Pimpl idiom's key benefit: hiding implementation details from the header. The destructor must be declared (and defined in the .cpp file) because std::unique_ptr needs to know how to delete Impl, which is an incomplete type in the header. Users of Widget see only the public interface - they have no idea what private members or dependencies exist in the implementation.
    // Move operations
    Widget(Widget&& other) noexcept;
    Widget& operator=(Widget&& other) noexcept;
    
    // Public interface
    void doSomething();
    std::string getName() const;
Move operations are declared but not defaulted in the header because std::unique_ptr needs a complete type to move. They'll be defaulted in the .cpp file where Impl is fully defined. Copy operations are implicitly deleted because std::unique_ptr is not copyable - if you need copying, you'd have to implement deep cloning manually. The public interface declares only the methods users need - all implementation is hidden behind this minimal API.
private:
    class Impl;  // Forward declaration only!
    std::unique_ptr<Impl> pImpl_;
};

#endif
The private section contains only a forward declaration of Impl and a unique_ptr to it. This is the essence of Pimpl: the pointer implementation. The actual Impl class definition is in the .cpp file, completely hidden from header users. When you change Impl's private members or include different headers in the implementation, only Widget.cpp recompiles - files that include Widget.h don't see any changes. In large projects, this dramatically reduces build times. The unique_ptr handles memory management automatically, making Pimpl exception-safe.
// Widget.cpp - Implementation file (all the details hidden here)
#include "Widget.h"
#include <iostream>
#include <vector>  // Only needed in .cpp, not exposed in header

class Widget::Impl {
public:
    Impl(const std::string& name) : name_(name) {}
    
    void doSomething() {
        std::cout << "Widget " << name_ << " doing something!" << std::endl;
        for (int i = 0; i < 3; ++i) {
            data_.push_back(i);
        }
    }
    
    std::string getName() const { return name_; }
    
private:
    std::string name_;
    std::vector<int> data_;  // Implementation detail hidden from header
};
The Impl class contains all implementation details. Notice <iostream> and <vector> are only included in the .cpp file - header users don't pay for these dependencies. The data_ vector is a private member that header users never see. If you change vector<int> to list<int> or add ten more private members, only Widget.cpp recompiles. This compilation firewall is invaluable in large projects where changing a widely-included header can trigger rebuilding thousands of files. Pimpl isolates implementation changes.
Widget::Widget(const std::string& name) 
    : pImpl_(std::make_unique<Impl>(name)) {}

Widget::~Widget() = default;

Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
The Widget constructor creates the Impl object, and member functions forward calls to it. The destructor, move constructor, and move assignment are defaulted in the .cpp file where Impl is complete. This is necessary because std::unique_ptr's destructor needs to delete Impl, requiring the complete type. The forwarding functions simply delegate to pImpl_ - they're trivial wrappers that add one level of indirection. This small runtime cost (pointer dereference) is usually negligible compared to the compilation time savings.
void Widget::doSomething() {
    pImpl_->doSomething();
}

std::string Widget::getName() const {
    return pImpl_->getName();
}
Each public method forwards to the corresponding Impl method through the pImpl_ pointer. This forwarding is the pattern's main runtime cost - an extra pointer dereference per call. However, modern CPUs handle this efficiently, and the compiler can often inline these forwarding functions. The trade-off is clear: slightly higher runtime cost (one pointer dereference) for dramatically lower compile-time cost (isolated implementation changes). Pimpl is most valuable for widely-used classes with frequently changing implementations in large codebases.

Practice Questions: Modern C++ Idioms

Problem: Create a Timer class that prints elapsed time when it goes out of scope.

Requirements:

  • Constructor stores current time
  • Destructor prints "Elapsed: X ms"
  • Use std::chrono for time measurement
Show Solution
#include <iostream>
#include <chrono>
#include <thread>

class Timer {
public:
    Timer() : start_(std::chrono::high_resolution_clock::now()) {}
    
    ~Timer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
            end - start_);
        std::cout << "Elapsed: " << duration.count() << " ms" << std::endl;
    }
    
private:
    std::chrono::high_resolution_clock::time_point start_;
};

int main() {
    {
        Timer t;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        // Destructor prints elapsed time
    }  // Elapsed: ~100 ms
    
    return 0;
}

Problem: Create a CRTP base class that adds comparison operators.

Requirements:

  • Derived class only implements compareTo() returning -1, 0, or 1
  • Base class provides <, >, <=, >=, ==, !=
Show Solution
#include <iostream>

template<typename Derived>
class Comparable {
public:
    bool operator<(const Derived& other) const {
        return derived().compareTo(other) < 0;
    }
    bool operator>(const Derived& other) const {
        return derived().compareTo(other) > 0;
    }
    bool operator<=(const Derived& other) const {
        return derived().compareTo(other) <= 0;
    }
    bool operator>=(const Derived& other) const {
        return derived().compareTo(other) >= 0;
    }
    bool operator==(const Derived& other) const {
        return derived().compareTo(other) == 0;
    }
    bool operator!=(const Derived& other) const {
        return derived().compareTo(other) != 0;
    }
    
private:
    const Derived& derived() const {
        return static_cast<const Derived&>(*this);
    }
};

class Person : public Comparable<Person> {
public:
    Person(int age) : age_(age) {}
    
    int compareTo(const Person& other) const {
        if (age_ < other.age_) return -1;
        if (age_ > other.age_) return 1;
        return 0;
    }
    
private:
    int age_;
};

int main() {
    Person p1(25), p2(30);
    
    std::cout << (p1 < p2) << std::endl;   // 1 (true)
    std::cout << (p1 == p2) << std::endl;  // 0 (false)
}

Problem: Create a Database connection class using Pimpl to hide implementation details.

Requirements:

  • Header file with minimal includes
  • Implementation file with actual connection logic
  • Methods: connect(), disconnect(), query()
  • Use std::unique_ptr for the Impl pointer
Show Solution
// Database.h
#ifndef DATABASE_H
#define DATABASE_H

#include <memory>
#include <string>

class Database {
public:
    Database();
    ~Database();
    
    Database(Database&&) noexcept;
    Database& operator=(Database&&) noexcept;
    
    bool connect(const std::string& connectionString);
    void disconnect();
    bool query(const std::string& sql);
    bool isConnected() const;
    
private:
    class Impl;
    std::unique_ptr<Impl> pImpl_;
};

#endif

// Database.cpp
#include "Database.h"
#include <iostream>
#include <vector>  // Hidden from header

class Database::Impl {
public:
    bool connect(const std::string& connStr) {
        std::cout << "Connecting to: " << connStr << std::endl;
        connectionString_ = connStr;
        connected_ = true;
        return true;
    }
    
    void disconnect() {
        if (connected_) {
            std::cout << "Disconnecting..." << std::endl;
            connected_ = false;
        }
    }
    
    bool query(const std::string& sql) {
        if (!connected_) return false;
        std::cout << "Executing: " << sql << std::endl;
        queryHistory_.push_back(sql);
        return true;
    }
    
    bool isConnected() const { return connected_; }
    
private:
    std::string connectionString_;
    bool connected_ = false;
    std::vector<std::string> queryHistory_;  // Hidden detail
};

Database::Database() : pImpl_(std::make_unique<Impl>()) {}
Database::~Database() = default;
Database::Database(Database&&) noexcept = default;
Database& Database::operator=(Database&&) noexcept = default;

bool Database::connect(const std::string& cs) { return pImpl_->connect(cs); }
void Database::disconnect() { pImpl_->disconnect(); }
bool Database::query(const std::string& sql) { return pImpl_->query(sql); }
bool Database::isConnected() const { return pImpl_->isConnected(); }

// main.cpp
int main() {
    Database db;
    db.connect("mysql://localhost:3306/mydb");
    db.query("SELECT * FROM users");
    db.disconnect();
}

Problem: Create an RAII wrapper that automatically closes database connections.

Requirements:

  • Constructor opens connection and prints status
  • Destructor automatically closes connection
  • Delete copy operations (non-copyable)
  • Method execute() to run queries
Show Solution
#include <iostream>
#include <string>
#include <stdexcept>

class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& connStr) 
        : connectionString_(connStr), connected_(false) {
        // Simulate connection
        std::cout << "Opening connection to: " << connStr << std::endl;
        connected_ = true;
        std::cout << "Connection established!" << std::endl;
    }
    
    ~DatabaseConnection() {
        if (connected_) {
            std::cout << "Closing connection to: " << connectionString_ << std::endl;
            connected_ = false;
            std::cout << "Connection closed." << std::endl;
        }
    }
    
    // Non-copyable
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    
    // Allow move
    DatabaseConnection(DatabaseConnection&& other) noexcept 
        : connectionString_(std::move(other.connectionString_)),
          connected_(other.connected_) {
        other.connected_ = false;
    }
    
    void execute(const std::string& query) {
        if (!connected_) {
            throw std::runtime_error("Not connected!");
        }
        std::cout << "Executing: " << query << std::endl;
    }
    
    bool isConnected() const { return connected_; }
    
private:
    std::string connectionString_;
    bool connected_;
};

void processData() {
    DatabaseConnection conn("mysql://localhost/test");
    conn.execute("SELECT * FROM users");
    conn.execute("UPDATE users SET active=1");
    // Connection automatically closed when conn goes out of scope
}

int main() {
    std::cout << "=== Starting process ===" << std::endl;
    processData();
    std::cout << "=== Process complete ===" << std::endl;
    // Output shows connection is properly closed
}

Key Takeaways

Singleton for Global Access

Use Singleton when you need exactly one instance with global access. Use Meyers Singleton for thread safety.

Factory for Flexibility

Use Factory to decouple object creation from usage. Makes adding new types easy without changing client code.

Observer for Events

Use Observer for one-to-many notifications. Perfect for event systems, MVC, and reactive programming.

Strategy for Algorithms

Use Strategy when you need interchangeable algorithms. Switch behavior at runtime without conditionals.

RAII for Resources

Always use RAII for resource management. Let destructors handle cleanup automatically - no leaks, no forgotten unlocks.

Decorator for Extensions

Use Decorator to add behavior dynamically. Compose features at runtime without subclass explosion.

Knowledge Check

Quick Quiz

Test what you've learned about C++ design patterns

1 Which pattern ensures a class has only one instance?
2 What does RAII stand for?
3 Which pattern is best for adding toppings to a pizza object at runtime?
4 In the Observer pattern, what notifies the observers of changes?
5 Which pattern would you use to switch between sorting algorithms at runtime?
6 What is the main benefit of the Pimpl idiom?
Answer all questions to check your score