Design Patterns in Modern C++

What Are Design Patterns?

Design patterns are like architectural blueprints for software. Just as architects reuse proven building designs (like the ranch house or Victorian style), programmers use design patterns to solve common problems elegantly.

Singleton Pattern: One Instance to Rule Them All

The Singleton ensures only one instance of a class exists, like having only one president of a country at a time.

Modern Thread-Safe Singleton

// Modern C++ Singleton using static local variable
class Logger {
private:
    Logger() {
        // Private constructor
        std::cout << "Logger created\n";
    }
    
public:
    // Delete copy/move operations
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
    Logger(Logger&&) = delete;
    Logger& operator=(Logger&&) = delete;
    
    // Thread-safe in C++11 and later
    static Logger& getInstance() {
        static Logger instance;  // Created once, destroyed at program end
        return instance;
    }
    
    void log(const std::string& message) {
        // Thread-safe logging would require additional synchronization
        std::cout << "[LOG] " << message << std::endl;
    }
};

// Usage
Logger::getInstance().log("Application started");
// Logger::getInstance() always returns the same instance
graph TD A[First Call to getInstance] --> B[Create Static Instance] B --> C[Return Reference] D[Subsequent Calls] --> C E[Program End] --> F[Automatic Destruction]

Factory Pattern: Object Creation Made Flexible

Factory pattern is like a restaurant menu - you order by name ("pizza") without knowing the exact preparation details.

// Abstract Product
class Vehicle {
public:
    virtual ~Vehicle() = default;
    virtual void drive() const = 0;
    virtual std::string getType() const = 0;
};

// Concrete Products
class Car : public Vehicle {
public:
    void drive() const override {
        std::cout << "Driving a car on the road\n";
    }
    std::string getType() const override { return "Car"; }
};

class Motorcycle : public Vehicle {
public:
    void drive() const override {
        std::cout << "Riding a motorcycle\n";
    }
    std::string getType() const override { return "Motorcycle"; }
};

// Modern Factory using unique_ptr and variadic templates
class VehicleFactory {
public:
    // Template factory method
    template<typename T, typename... Args>
    static std::unique_ptr<Vehicle> create(Args&&... args) {
        return std::make_unique<T>(std::forward<Args>(args)...);
    }
    
    // String-based factory
    static std::unique_ptr<Vehicle> create(const std::string& type) {
        if (type == "car") {
            return std::make_unique<Car>();
        } else if (type == "motorcycle") {
            return std::make_unique<Motorcycle>();
        }
        return nullptr;
    }
};

// Usage
auto car = VehicleFactory::create<Car>();
auto moto = VehicleFactory::create("motorcycle");
car->drive();

Observer Pattern: Event-Driven Communication

Observer pattern is like a newsletter subscription - subscribers get notified when new content is published.

// Modern Observer using function objects and weak_ptr
template<typename EventType>
class EventEmitter {
private:
    using Handler = std::function<void(const EventType&)>;
    using HandlerPtr = std::shared_ptr<Handler>;
    mutable std::mutex mutex_;
    std::vector<std::weak_ptr<Handler>> handlers_;
    
public:
    class Subscription {
        friend class EventEmitter;
        std::weak_ptr<Handler> handler_;
        EventEmitter* emitter_;
        
        Subscription(std::weak_ptr<Handler> h, EventEmitter* e)
            : handler_(h), emitter_(e) {}
        
    public:
        void unsubscribe() {
            if (auto h = handler_.lock()) {
                emitter_->remove(h);
            }
        }
    };
    
    Subscription subscribe(Handler handler) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto ptr = std::make_shared<Handler>(std::move(handler));
        handlers_.push_back(ptr);
        return Subscription(ptr, this);
    }
    
    void emit(const EventType& event) const {
        std::lock_guard<std::mutex> lock(mutex_);
        // Clean up expired handlers
        auto it = std::remove_if(handlers_.begin(), handlers_.end(),
            [](const std::weak_ptr<Handler>& h) { return h.expired(); });
        handlers_.erase(it, handlers_.end());
        
        // Notify active handlers
        for (const auto& weakHandler : handlers_) {
            if (auto handler = weakHandler.lock()) {
                (*handler)(event);
            }
        }
    }
    
private:
    void remove(const HandlerPtr& target) {
        std::lock_guard<std::mutex> lock(mutex_);
        handlers_.erase(
            std::remove_if(handlers_.begin(), handlers_.end(),
                [&](const std::weak_ptr<Handler>& h) {
                    auto ptr = h.lock();
                    return !ptr || ptr == target;
                }),
            handlers_.end()
        );
    }
};

// Usage Example
struct PriceChangeEvent {
    std::string symbol;
    double oldPrice;
    double newPrice;
};

class StockMarket {
    EventEmitter<PriceChangeEvent> priceChanged;
    
public:
    auto onPriceChange(std::function<void(const PriceChangeEvent&)> handler) {
        return priceChanged.subscribe(std::move(handler));
    }
    
    void updatePrice(const std::string& symbol, double newPrice) {
        double oldPrice = getPrice(symbol);
        setPrice(symbol, newPrice);
        priceChanged.emit({symbol, oldPrice, newPrice});
    }
    
private:
    std::map<std::string, double> prices_;
    double getPrice(const std::string& symbol) { /* ... */ }
    void setPrice(const std::string& symbol, double price) { /* ... */ }
};
graph LR A[Subject
StockMarket] --> B[Event:
Price Changed] B --> C[Observer 1
Logger] B --> D[Observer 2
Trader] B --> E[Observer 3
Display]

Strategy Pattern: Algorithms as Objects

Strategy pattern is like choosing a route to work - you can walk, drive, or take the bus, each with different characteristics.

// Modern Strategy using std::function and lambdas
class DataProcessor {
private:
    using ProcessingStrategy = std::function<std::vector<int>(const std::vector<int>&)>;
    ProcessingStrategy strategy_;
    
public:
    void setStrategy(ProcessingStrategy strategy) {
        strategy_ = std::move(strategy);
    }
    
    std::vector<int> process(const std::vector<int>& data) {
        if (!strategy_) {
            throw std::logic_error("No strategy set");
        }
        return strategy_(data);
    }
};

// Predefined strategies
namespace Strategies {
    auto sortAscending = [](const std::vector<int>& data) {
        auto result = data;
        std::sort(result.begin(), result.end());
        return result;
    };
    
    auto sortDescending = [](const std::vector<int>& data) {
        auto result = data;
        std::sort(result.rbegin(), result.rend());
        return result;
    };
    
    auto removeDuplicates = [](const std::vector<int>& data) {
        auto result = data;
        std::sort(result.begin(), result.end());
        result.erase(std::unique(result.begin(), result.end()), result.end());
        return result;
    };
    
    auto filterEven = [](const std::vector<int>& data) {
        std::vector<int> result;
        std::copy_if(data.begin(), data.end(), std::back_inserter(result),
                     [](int n) { return n % 2 == 0; });
        return result;
    };
}

// Usage
DataProcessor processor;
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};

processor.setStrategy(Strategies::sortAscending);
auto sorted = processor.process(data);  // [1, 1, 2, 3, 4, 5, 6, 9]

processor.setStrategy(Strategies::removeDuplicates);
auto unique = processor.process(data);  // [1, 2, 3, 4, 5, 6, 9]

// Custom strategy on the fly
processor.setStrategy([](const auto& data) {
    std::vector<int> result;
    for (int n : data) {
        result.push_back(n * n);  // Square each element
    }
    return result;
});
auto squared = processor.process(data);

Decorator Pattern: Adding Features Dynamically

Decorator pattern is like adding toppings to a pizza - each topping adds to the base without changing its fundamental nature.

// Modern Decorator using templates and perfect forwarding
template<typename Component>
class TextProcessor {
public:
    virtual ~TextProcessor() = default;
    virtual std::string process(const std::string& text) const = 0;
};

// Base component
class PlainText : public TextProcessor<PlainText> {
public:
    std::string process(const std::string& text) const override {
        return text;
    }
};

// Decorator base class
template<typename Component>
class TextDecorator : public TextProcessor<Component> {
protected:
    std::unique_ptr<TextProcessor<Component>> component_;
    
public:
    explicit TextDecorator(std::unique_ptr<TextProcessor<Component>> component)
        : component_(std::move(component)) {}
};

// Concrete decorators
template<typename Component>
class UpperCaseDecorator : public TextDecorator<Component> {
public:
    using TextDecorator<Component>::TextDecorator;
    
    std::string process(const std::string& text) const override {
        auto result = this->component_->process(text);
        std::transform(result.begin(), result.end(), result.begin(), ::toupper);
        return result;
    }
};

template<typename Component>
class TrimDecorator : public TextDecorator<Component> {
public:
    using TextDecorator<Component>::TextDecorator;
    
    std::string process(const std::string& text) const override {
        auto result = this->component_->process(text);
        // Trim leading/trailing whitespace
        result.erase(0, result.find_first_not_of(" \t\n\r"));
        result.erase(result.find_last_not_of(" \t\n\r") + 1);
        return result;
    }
};

// Fluent builder for easy decoration
template<typename T>
class TextProcessorBuilder {
    std::unique_ptr<TextProcessor<T>> processor_;
    
public:
    TextProcessorBuilder() : processor_(std::make_unique<PlainText>()) {}
    
    TextProcessorBuilder& upperCase() {
        processor_ = std::make_unique<UpperCaseDecorator<T>>(std::move(processor_));
        return *this;
    }
    
    TextProcessorBuilder& trim() {
        processor_ = std::make_unique<TrimDecorator<T>>(std::move(processor_));
        return *this;
    }
    
    std::unique_ptr<TextProcessor<T>> build() {
        return std::move(processor_);
    }
};

// Usage
auto processor = TextProcessorBuilder<PlainText>()
    .trim()
    .upperCase()
    .build();

std::string result = processor->process("  hello world  ");
// Result: "HELLO WORLD"

RAII and Smart Pointer Patterns

RAII (Resource Acquisition Is Initialization) is like hiring a cleaning service that automatically cleans when they leave - resources are automatically managed.

// Custom RAII wrapper for file handles
class FileHandle {
private:
    FILE* file_;
    
public:
    explicit FileHandle(const std::string& filename, const char* mode)
        : file_(std::fopen(filename.c_str(), mode)) {
        if (!file_) {
            throw std::runtime_error("Failed to open file: " + filename);
        }
    }
    
    ~FileHandle() {
        if (file_) {
            std::fclose(file_);
        }
    }
    
    // Delete copy operations
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // Move operations
    FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }
    
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file_) std::fclose(file_);
            file_ = other.file_;
            other.file_ = nullptr;
        }
        return *this;
    }
    
    FILE* get() const { return file_; }
    operator FILE*() const { return file_; }
};

// Custom deleter for unique_ptr
struct SocketDeleter {
    void operator()(int* socket) const {
        if (socket && *socket >= 0) {
            close(*socket);
            delete socket;
        }
    }
};

using SocketPtr = std::unique_ptr<int, SocketDeleter>;

// Shared resource with custom deleter
auto createSharedBuffer(size_t size) {
    return std::shared_ptr<uint8_t[]>(
        new uint8_t[size],
        [](uint8_t* p) { 
            std::cout << "Deallocating buffer\n";
            delete[] p; 
        }
    );
}

Template Method Pattern with CRTP

The Curiously Recurring Template Pattern (CRTP) provides compile-time polymorphism, like a recipe template where specific steps are filled in by the chef.

// CRTP base class for game entities
template<typename Derived>
class GameObject {
public:
    void update(float deltaTime) {
        // Common pre-update logic
        if (!isActive()) return;
        
        // Derived-specific update
        static_cast<Derived*>(this)->onUpdate(deltaTime);
        
        // Common post-update logic
        updateAnimation(deltaTime);
    }
    
    void render() {
        if (!isVisible()) return;
        
        beginRender();
        static_cast<Derived*>(this)->onRender();
        endRender();
    }
    
protected:
    bool isActive() const { return active_; }
    bool isVisible() const { return visible_; }
    
private:
    bool active_ = true;
    bool visible_ = true;
    
    void updateAnimation(float deltaTime) { /* ... */ }
    void beginRender() { /* ... */ }
    void endRender() { /* ... */ }
};

// Concrete implementations
class Player : public GameObject<Player> {
    friend class GameObject<Player>;
    
    void onUpdate(float deltaTime) {
        // Player-specific update logic
        handleInput();
        movePlayer(deltaTime);
    }
    
    void onRender() {
        // Player-specific rendering
        drawSprite(sprite_, position_);
    }
    
    void handleInput() { /* ... */ }
    void movePlayer(float deltaTime) { /* ... */ }
};

class Enemy : public GameObject<Enemy> {
    friend class GameObject<Enemy>;
    
    void onUpdate(float deltaTime) {
        // Enemy-specific update logic
        updateAI(deltaTime);
        moveTowardsPlayer(deltaTime);
    }
    
    void onRender() {
        // Enemy-specific rendering
        drawAnimatedSprite(animation_, position_);
    }
    
    void updateAI(float deltaTime) { /* ... */ }
    void moveTowardsPlayer(float deltaTime) { /* ... */ }
};

Modern Visitor Pattern with std::variant

The visitor pattern allows operations on objects without modifying their classes. With std::variant, it's like having a universal remote that works differently for each device.

// Define shape types
struct Circle {
    double radius;
};

struct Rectangle {
    double width, height;
};

struct Triangle {
    double base, height;
};

// Shape variant
using Shape = std::variant<Circle, Rectangle, Triangle>;

// Visitor for calculating area
struct AreaCalculator {
    double operator()(const Circle& c) const {
        return M_PI * c.radius * c.radius;
    }
    
    double operator()(const Rectangle& r) const {
        return r.width * r.height;
    }
    
    double operator()(const Triangle& t) const {
        return 0.5 * t.base * t.height;
    }
};

// Visitor for drawing
struct ShapeDrawer {
    void operator()(const Circle& c) const {
        std::cout << "Drawing circle with radius " << c.radius << "\n";
    }
    
    void operator()(const Rectangle& r) const {
        std::cout << "Drawing rectangle " << r.width << "x" << r.height << "\n";
    }
    
    void operator()(const Triangle& t) const {
        std::cout << "Drawing triangle with base " << t.base << "\n";
    }
};

// Usage
std::vector<Shape> shapes = {
    Circle{5.0},
    Rectangle{10.0, 20.0},
    Triangle{8.0, 6.0}
};

// Calculate total area
double totalArea = 0;
for (const auto& shape : shapes) {
    totalArea += std::visit(AreaCalculator{}, shape);
}

// Draw all shapes
for (const auto& shape : shapes) {
    std::visit(ShapeDrawer{}, shape);
}

Practical Examples

Exercise: Command Pattern with Undo/Redo

// Implement a command pattern for a text editor
class TextEditor {
    std::string text_;
public:
    void insert(size_t pos, const std::string& str) { /* ... */ }
    void erase(size_t pos, size_t len) { /* ... */ }
    const std::string& getText() const { return text_; }
};

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

// TODO: Implement InsertCommand and DeleteCommand
// TODO: Implement CommandHistory with undo/redo stacks

Exercise: Builder Pattern for Complex Objects

// Create a fluent builder for HTTP requests
class HttpRequest {
    // Private members: method, url, headers, body, etc.
public:
    class Builder {
        // TODO: Implement builder methods
        // Builder& method(const std::string& m);
        // Builder& url(const std::string& u);
        // Builder& header(const std::string& key, const std::string& value);
        // Builder& body(const std::string& b);
        // HttpRequest build();
    };
    
    static Builder create() { return Builder{}; }
};

// Usage should look like:
// auto request = HttpRequest::create()
//     .method("POST")
//     .url("https://api.example.com/data")
//     .header("Content-Type", "application/json")
//     .body("{\"key\": \"value\"}")
//     .build();

Best Practices and Guidelines

graph TD A[Choosing a Pattern] --> B{What problem?} B --> C[Object Creation] B --> D[Object Structure] B --> E[Object Behavior] C --> F[Factory/Builder/Singleton] D --> G[Adapter/Decorator/Composite] E --> H[Strategy/Observer/Command] I[Modern C++ Features] --> J[Prefer composition] I --> K[Use smart pointers] I --> L[Leverage templates] I --> M[Consider std::variant]

Modern C++ Pattern Guidelines

Next Steps

You've learned how modern C++ enhances classic design patterns:

Design patterns in modern C++ are more flexible and efficient than ever. Smart pointers eliminate memory leaks, templates provide compile-time safety, and features like std::variant offer new ways to implement classic patterns. In our final lesson, we'll explore cutting-edge C++20 features including concepts, ranges, and coroutines.