Skip to content

Design Patterns in C++

Design patterns are proven solutions to common software design problems. This guide covers essential patterns frequently used in embedded systems and validation software development.


Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides global access to it. Common uses include logger, configuration manager, hardware abstraction layer, and resource managers.

Basic Implementation

#include <mutex>
#include <memory>

// ============================================================
// SINGLETON - Meyer's Singleton (Thread-Safe in C++11+)
// ============================================================
class Logger {
public:
    // Delete copy constructor and assignment operator
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    // Static method to get the single instance
    static Logger& getInstance() {
        static Logger instance;  // Thread-safe in C++11+
        return instance;
    }

    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << "[LOG] " << message << std::endl;
    }

    void setLogLevel(int level) {
        log_level_ = level;
    }

private:
    // Private constructor
    Logger() : log_level_(0) {
        std::cout << "Logger initialized" << std::endl;
    }

    ~Logger() {
        std::cout << "Logger destroyed" << std::endl;
    }

    std::mutex mutex_;
    int log_level_;
};

// Usage
void someFunction() {
    Logger::getInstance().log("Application started");
    Logger::getInstance().setLogLevel(2);
    Logger::getInstance().log("Level set to 2");
}

Thread-Safe Singleton with Double-Checked Locking

#include <mutex>
#include <atomic>

class ConfigManager {
public:
    ConfigManager(const ConfigManager&) = delete;
    ConfigManager& operator=(const ConfigManager&) = delete;

    static ConfigManager* getInstance() {
        // First check (without lock)
        ConfigManager* tmp = instance_.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            // Second check (with lock)
            tmp = instance_.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new ConfigManager();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

    // Configuration methods
    void loadConfig(const std::string& filename) {
        // Load configuration from file
    }

    int getIntValue(const std::string& key, int defaultVal = 0) {
        auto it = intConfig_.find(key);
        return (it != intConfig_.end()) ? it->second : defaultVal;
    }

    void setIntValue(const std::string& key, int value) {
        std::lock_guard<std::mutex> lock(configMutex_);
        intConfig_[key] = value;
    }

private:
    ConfigManager() = default;

    static std::atomic<ConfigManager*> instance_;
    static std::mutex mutex_;

    std::mutex configMutex_;
    std::unordered_map<std::string, int> intConfig_;
};

// Static member initialization
std::atomic<ConfigManager*> ConfigManager::instance_{nullptr};
std::mutex ConfigManager::mutex_;

Singleton Template

// Reusable Singleton template
template<typename T>
class Singleton {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static T& getInstance() {
        static T instance;
        return instance;
    }

protected:
    Singleton() = default;
    virtual ~Singleton() = default;
};

// Usage: Inherit from Singleton
class HardwareManager : public Singleton<HardwareManager> {
    friend class Singleton<HardwareManager>;

public:
    void initializeHardware() {
        std::cout << "Hardware initialized" << std::endl;
    }

    uint32_t readRegister(uint32_t addr) {
        return 0;  // Actual implementation
    }

private:
    HardwareManager() = default;
};

// Usage
void example() {
    HardwareManager::getInstance().initializeHardware();
    uint32_t val = HardwareManager::getInstance().readRegister(0x1000);
}

Practical Example: Device Registry

#include <unordered_map>
#include <memory>
#include <mutex>

class Device {
public:
    virtual ~Device() = default;
    virtual void initialize() = 0;
    virtual std::string getName() const = 0;
};

class DeviceRegistry {
public:
    static DeviceRegistry& getInstance() {
        static DeviceRegistry instance;
        return instance;
    }

    DeviceRegistry(const DeviceRegistry&) = delete;
    DeviceRegistry& operator=(const DeviceRegistry&) = delete;

    void registerDevice(const std::string& id, std::shared_ptr<Device> device) {
        std::lock_guard<std::mutex> lock(mutex_);
        devices_[id] = device;
    }

    std::shared_ptr<Device> getDevice(const std::string& id) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = devices_.find(id);
        return (it != devices_.end()) ? it->second : nullptr;
    }

    void initializeAll() {
        std::lock_guard<std::mutex> lock(mutex_);
        for (auto& [id, device] : devices_) {
            device->initialize();
        }
    }

private:
    DeviceRegistry() = default;

    std::mutex mutex_;
    std::unordered_map<std::string, std::shared_ptr<Device>> devices_;
};

Factory Pattern

The Factory pattern provides an interface for creating objects without specifying their exact class. It's useful when object creation involves complex logic or when the system needs to be independent of how objects are created.

Simple Factory

#include <memory>
#include <string>
#include <stdexcept>

// Product interface
class MemoryDevice {
public:
    virtual ~MemoryDevice() = default;
    virtual void initialize() = 0;
    virtual void read(uint32_t address, uint8_t* buffer, size_t size) = 0;
    virtual void write(uint32_t address, const uint8_t* data, size_t size) = 0;
    virtual std::string getType() const = 0;
};

// Concrete products
class DDR4Device : public MemoryDevice {
public:
    void initialize() override {
        std::cout << "Initializing DDR4 device" << std::endl;
        // DDR4-specific initialization
    }

    void read(uint32_t address, uint8_t* buffer, size_t size) override {
        // DDR4 read implementation
    }

    void write(uint32_t address, const uint8_t* data, size_t size) override {
        // DDR4 write implementation
    }

    std::string getType() const override { return "DDR4"; }
};

class HighSpeedDevice : public MemoryDevice {
public:
    void initialize() override {
        std::cout << "Initializing HighSpeed device" << std::endl;
        // HighSpeed-specific initialization
    }

    void read(uint32_t address, uint8_t* buffer, size_t size) override {
        // HighSpeed read implementation
    }

    void write(uint32_t address, const uint8_t* data, size_t size) override {
        // HighSpeed write implementation
    }

    std::string getType() const override { return "HighSpeed"; }
};

class LowPowerDevice : public MemoryDevice {
public:
    void initialize() override {
        std::cout << "Initializing LowPower device" << std::endl;
    }

    void read(uint32_t address, uint8_t* buffer, size_t size) override { }
    void write(uint32_t address, const uint8_t* data, size_t size) override { }
    std::string getType() const override { return "LowPower"; }
};

// Simple Factory
class MemoryDeviceFactory {
public:
    static std::unique_ptr<MemoryDevice> createDevice(const std::string& type) {
        if (type == "DDR4") {
            return std::make_unique<DDR4Device>();
        } else if (type == "HighSpeed") {
            return std::make_unique<HighSpeedDevice>();
        } else if (type == "LowPower") {
            return std::make_unique<LowPowerDevice>();
        }
        throw std::invalid_argument("Unknown device type: " + type);
    }
};

// Usage
void example() {
    auto ddr4 = MemoryDeviceFactory::createDevice("DDR4");
    ddr4->initialize();

    auto highSpeed = MemoryDeviceFactory::createDevice("HighSpeed");
    highSpeed->initialize();
}

Factory Method Pattern

// Abstract creator
class DeviceCreator {
public:
    virtual ~DeviceCreator() = default;

    // Factory method
    virtual std::unique_ptr<MemoryDevice> createDevice() = 0;

    // Template method using the factory method
    void initializeAndTest() {
        auto device = createDevice();
        device->initialize();
        runDiagnostics(device.get());
    }

protected:
    virtual void runDiagnostics(MemoryDevice* device) {
        std::cout << "Running diagnostics on " << device->getType() << std::endl;
    }
};

// Concrete creators
class DDR4Creator : public DeviceCreator {
public:
    std::unique_ptr<MemoryDevice> createDevice() override {
        return std::make_unique<DDR4Device>();
    }

protected:
    void runDiagnostics(MemoryDevice* device) override {
        DeviceCreator::runDiagnostics(device);
        std::cout << "Running DDR4-specific tests" << std::endl;
    }
};

class HighSpeedCreator : public DeviceCreator {
public:
    std::unique_ptr<MemoryDevice> createDevice() override {
        return std::make_unique<HighSpeedDevice>();
    }
};

// Usage
void example() {
    std::unique_ptr<DeviceCreator> creator = std::make_unique<DDR4Creator>();
    creator->initializeAndTest();

    creator = std::make_unique<HighSpeedCreator>();
    creator->initializeAndTest();
}

Abstract Factory Pattern

// Abstract products
class MemoryController {
public:
    virtual ~MemoryController() = default;
    virtual void configure() = 0;
};

class PhyInterface {
public:
    virtual ~PhyInterface() = default;
    virtual void train() = 0;
};

// DDR4 family
class DDR4Controller : public MemoryController {
public:
    void configure() override {
        std::cout << "Configuring DDR4 controller" << std::endl;
    }
};

class DDR4Phy : public PhyInterface {
public:
    void train() override {
        std::cout << "Training DDR4 PHY" << std::endl;
    }
};

// HighSpeed family
class HighSpeedController : public MemoryController {
public:
    void configure() override {
        std::cout << "Configuring HighSpeed controller" << std::endl;
    }
};

class HighSpeedPhy : public PhyInterface {
public:
    void train() override {
        std::cout << "Training HighSpeed PHY" << std::endl;
    }
};

// Abstract Factory
class MemorySubsystemFactory {
public:
    virtual ~MemorySubsystemFactory() = default;
    virtual std::unique_ptr<MemoryController> createController() = 0;
    virtual std::unique_ptr<PhyInterface> createPhy() = 0;
    virtual std::unique_ptr<MemoryDevice> createDevice() = 0;
};

// Concrete Factories
class DDR4Factory : public MemorySubsystemFactory {
public:
    std::unique_ptr<MemoryController> createController() override {
        return std::make_unique<DDR4Controller>();
    }

    std::unique_ptr<PhyInterface> createPhy() override {
        return std::make_unique<DDR4Phy>();
    }

    std::unique_ptr<MemoryDevice> createDevice() override {
        return std::make_unique<DDR4Device>();
    }
};

class HighSpeedFactory : public MemorySubsystemFactory {
public:
    std::unique_ptr<MemoryController> createController() override {
        return std::make_unique<HighSpeedController>();
    }

    std::unique_ptr<PhyInterface> createPhy() override {
        return std::make_unique<HighSpeedPhy>();
    }

    std::unique_ptr<MemoryDevice> createDevice() override {
        return std::make_unique<HighSpeedDevice>();
    }
};

// Client code
class MemorySubsystem {
public:
    explicit MemorySubsystem(std::unique_ptr<MemorySubsystemFactory> factory)
        : controller_(factory->createController())
        , phy_(factory->createPhy())
        , device_(factory->createDevice()) {}

    void initialize() {
        controller_->configure();
        phy_->train();
        device_->initialize();
    }

private:
    std::unique_ptr<MemoryController> controller_;
    std::unique_ptr<PhyInterface> phy_;
    std::unique_ptr<MemoryDevice> device_;
};

// Usage
void example() {
    // Create HighSpeed subsystem
    auto highSpeedFactory = std::make_unique<HighSpeedFactory>();
    MemorySubsystem highSpeedSubsystem(std::move(highSpeedFactory));
    highSpeedSubsystem.initialize();

    // Create DDR4 subsystem
    auto ddr4Factory = std::make_unique<DDR4Factory>();
    MemorySubsystem ddr4Subsystem(std::move(ddr4Factory));
    ddr4Subsystem.initialize();
}

Registry-Based Factory

#include <functional>
#include <unordered_map>

class DeviceFactoryRegistry {
public:
    using Creator = std::function<std::unique_ptr<MemoryDevice>()>;

    static DeviceFactoryRegistry& getInstance() {
        static DeviceFactoryRegistry instance;
        return instance;
    }

    void registerCreator(const std::string& type, Creator creator) {
        creators_[type] = std::move(creator);
    }

    std::unique_ptr<MemoryDevice> create(const std::string& type) {
        auto it = creators_.find(type);
        if (it == creators_.end()) {
            throw std::invalid_argument("Unknown device type: " + type);
        }
        return it->second();
    }

    std::vector<std::string> getRegisteredTypes() const {
        std::vector<std::string> types;
        for (const auto& [type, creator] : creators_) {
            types.push_back(type);
        }
        return types;
    }

private:
    DeviceFactoryRegistry() = default;
    std::unordered_map<std::string, Creator> creators_;
};

// Auto-registration helper
template<typename T>
class DeviceRegistrar {
public:
    explicit DeviceRegistrar(const std::string& type) {
        DeviceFactoryRegistry::getInstance().registerCreator(type, []() {
            return std::make_unique<T>();
        });
    }
};

// Auto-register devices (in .cpp files)
static DeviceRegistrar<DDR4Device> ddr4Registrar("DDR4");
static DeviceRegistrar<HighSpeedDevice> highSpeedRegistrar("HighSpeed");
static DeviceRegistrar<LowPowerDevice> lowPowerRegistrar("LowPower");

// Usage
void example() {
    auto& registry = DeviceFactoryRegistry::getInstance();

    // List all registered types
    for (const auto& type : registry.getRegisteredTypes()) {
        std::cout << "Registered: " << type << std::endl;
    }

    // Create device by type string
    auto device = registry.create("HighSpeed");
    device->initialize();
}

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified. Common uses include event handling, GUI updates, and hardware interrupt notifications.

Basic Observer

#include <vector>
#include <algorithm>
#include <functional>
#include <memory>

// Observer interface
class IObserver {
public:
    virtual ~IObserver() = default;
    virtual void update(const std::string& event, void* data) = 0;
};

// Subject (Observable)
class Subject {
public:
    void attach(IObserver* observer) {
        observers_.push_back(observer);
    }

    void detach(IObserver* observer) {
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), observer),
            observers_.end()
        );
    }

    void notify(const std::string& event, void* data = nullptr) {
        for (auto* observer : observers_) {
            observer->update(event, data);
        }
    }

private:
    std::vector<IObserver*> observers_;
};

// Concrete Subject
class TemperatureSensor : public Subject {
public:
    void setTemperature(float temp) {
        temperature_ = temp;
        if (temp > threshold_) {
            notify("TEMP_HIGH", &temperature_);
        } else {
            notify("TEMP_NORMAL", &temperature_);
        }
    }

    float getTemperature() const { return temperature_; }

    void setThreshold(float threshold) { threshold_ = threshold; }

private:
    float temperature_ = 0.0f;
    float threshold_ = 85.0f;
};

// Concrete Observers
class DisplayPanel : public IObserver {
public:
    void update(const std::string& event, void* data) override {
        if (data) {
            float temp = *static_cast<float*>(data);
            std::cout << "Display: Temperature = " << temp << "°C" << std::endl;
        }
    }
};

class AlarmSystem : public IObserver {
public:
    void update(const std::string& event, void* data) override {
        if (event == "TEMP_HIGH") {
            float temp = *static_cast<float*>(data);
            std::cout << "ALARM: High temperature alert! " << temp << "°C" << std::endl;
            triggerAlarm();
        }
    }

private:
    void triggerAlarm() {
        std::cout << ">>> ALARM TRIGGERED <<<" << std::endl;
    }
};

class DataLogger : public IObserver {
public:
    void update(const std::string& event, void* data) override {
        if (data) {
            float temp = *static_cast<float*>(data);
            log_.push_back({event, temp});
            std::cout << "Logger: Recorded " << event << " = " << temp << std::endl;
        }
    }

    void printLog() const {
        std::cout << "\n=== Temperature Log ===" << std::endl;
        for (const auto& entry : log_) {
            std::cout << entry.event << ": " << entry.value << "°C" << std::endl;
        }
    }

private:
    struct LogEntry {
        std::string event;
        float value;
    };
    std::vector<LogEntry> log_;
};

// Usage
void example() {
    TemperatureSensor sensor;

    DisplayPanel display;
    AlarmSystem alarm;
    DataLogger logger;

    sensor.attach(&display);
    sensor.attach(&alarm);
    sensor.attach(&logger);

    sensor.setTemperature(75.0f);  // Normal
    sensor.setTemperature(90.0f);  // High - triggers alarm
    sensor.setTemperature(80.0f);  // Normal again

    sensor.detach(&alarm);  // Remove alarm

    sensor.setTemperature(95.0f);  // High, but no alarm

    logger.printLog();
}

Modern C++ Observer with std::function

#include <functional>
#include <unordered_map>
#include <vector>
#include <any>

class EventEmitter {
public:
    using Callback = std::function<void(const std::any&)>;
    using CallbackId = size_t;

    CallbackId on(const std::string& event, Callback callback) {
        CallbackId id = nextId_++;
        listeners_[event].push_back({id, std::move(callback)});
        return id;
    }

    void off(const std::string& event, CallbackId id) {
        auto it = listeners_.find(event);
        if (it != listeners_.end()) {
            auto& callbacks = it->second;
            callbacks.erase(
                std::remove_if(callbacks.begin(), callbacks.end(),
                    [id](const CallbackEntry& entry) {
                        return entry.id == id;
                    }),
                callbacks.end()
            );
        }
    }

    void emit(const std::string& event, const std::any& data = {}) {
        auto it = listeners_.find(event);
        if (it != listeners_.end()) {
            for (const auto& entry : it->second) {
                entry.callback(data);
            }
        }
    }

private:
    struct CallbackEntry {
        CallbackId id;
        Callback callback;
    };

    std::unordered_map<std::string, std::vector<CallbackEntry>> listeners_;
    CallbackId nextId_ = 0;
};

// Usage
void example() {
    EventEmitter emitter;

    // Lambda callback
    auto id1 = emitter.on("data_ready", [](const std::any& data) {
        auto value = std::any_cast<int>(data);
        std::cout << "Received data: " << value << std::endl;
    });

    // Another callback
    emitter.on("data_ready", [](const std::any& data) {
        std::cout << "Processing data..." << std::endl;
    });

    emitter.emit("data_ready", 42);

    emitter.off("data_ready", id1);  // Remove first callback

    emitter.emit("data_ready", 100);  // Only second callback fires
}

Hardware Interrupt Observer

#include <functional>
#include <vector>
#include <mutex>
#include <queue>
#include <thread>
#include <condition_variable>

// Interrupt event types
enum class InterruptType {
    DMA_COMPLETE,
    ERROR,
    DATA_READY,
    TIMER,
    GPIO
};

struct InterruptEvent {
    InterruptType type;
    uint32_t source;
    uint32_t data;
    uint64_t timestamp;
};

class InterruptController {
public:
    using Handler = std::function<void(const InterruptEvent&)>;

    static InterruptController& getInstance() {
        static InterruptController instance;
        return instance;
    }

    // Register handler for specific interrupt type
    void registerHandler(InterruptType type, Handler handler) {
        std::lock_guard<std::mutex> lock(mutex_);
        handlers_[type].push_back(std::move(handler));
    }

    // Called from ISR context (or simulated)
    void triggerInterrupt(InterruptType type, uint32_t source, uint32_t data) {
        InterruptEvent event{
            type,
            source,
            data,
            getCurrentTimestamp()
        };

        {
            std::lock_guard<std::mutex> lock(queueMutex_);
            eventQueue_.push(event);
        }
        cv_.notify_one();
    }

    // Process interrupts (called from main thread or dedicated thread)
    void processInterrupts() {
        std::unique_lock<std::mutex> lock(queueMutex_);

        cv_.wait(lock, [this] { return !eventQueue_.empty() || shutdown_; });

        while (!eventQueue_.empty()) {
            InterruptEvent event = eventQueue_.front();
            eventQueue_.pop();
            lock.unlock();

            dispatchEvent(event);

            lock.lock();
        }
    }

    void startProcessingThread() {
        processingThread_ = std::thread([this] {
            while (!shutdown_) {
                processInterrupts();
            }
        });
    }

    void shutdown() {
        shutdown_ = true;
        cv_.notify_all();
        if (processingThread_.joinable()) {
            processingThread_.join();
        }
    }

private:
    InterruptController() = default;

    void dispatchEvent(const InterruptEvent& event) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = handlers_.find(event.type);
        if (it != handlers_.end()) {
            for (const auto& handler : it->second) {
                handler(event);
            }
        }
    }

    uint64_t getCurrentTimestamp() {
        return std::chrono::duration_cast<std::chrono::microseconds>(
            std::chrono::steady_clock::now().time_since_epoch()
        ).count();
    }

    std::mutex mutex_;
    std::mutex queueMutex_;
    std::condition_variable cv_;
    std::unordered_map<InterruptType, std::vector<Handler>> handlers_;
    std::queue<InterruptEvent> eventQueue_;
    std::thread processingThread_;
    bool shutdown_ = false;
};

// Usage
void example() {
    auto& intCtrl = InterruptController::getInstance();

    // Register handlers
    intCtrl.registerHandler(InterruptType::DMA_COMPLETE,
        [](const InterruptEvent& event) {
            std::cout << "DMA complete from source " << event.source << std::endl;
        });

    intCtrl.registerHandler(InterruptType::ERROR,
        [](const InterruptEvent& event) {
            std::cout << "ERROR: code " << event.data << std::endl;
        });

    intCtrl.startProcessingThread();

    // Simulate interrupts
    intCtrl.triggerInterrupt(InterruptType::DMA_COMPLETE, 0, 0);
    intCtrl.triggerInterrupt(InterruptType::ERROR, 1, 0x1234);

    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    intCtrl.shutdown();
}

Type-Safe Observer with Templates

template<typename... Args>
class Signal {
public:
    using Slot = std::function<void(Args...)>;
    using SlotId = size_t;

    SlotId connect(Slot slot) {
        SlotId id = nextId_++;
        slots_.push_back({id, std::move(slot)});
        return id;
    }

    void disconnect(SlotId id) {
        slots_.erase(
            std::remove_if(slots_.begin(), slots_.end(),
                [id](const SlotEntry& entry) { return entry.id == id; }),
            slots_.end()
        );
    }

    void emit(Args... args) {
        for (const auto& entry : slots_) {
            entry.slot(args...);
        }
    }

    void operator()(Args... args) {
        emit(std::forward<Args>(args)...);
    }

private:
    struct SlotEntry {
        SlotId id;
        Slot slot;
    };

    std::vector<SlotEntry> slots_;
    SlotId nextId_ = 0;
};

// Usage
class Button {
public:
    Signal<> clicked;
    Signal<int, int> positionChanged;

    void click() {
        clicked.emit();
    }

    void moveTo(int x, int y) {
        positionChanged.emit(x, y);
    }
};

void example() {
    Button button;

    button.clicked.connect([]() {
        std::cout << "Button clicked!" << std::endl;
    });

    auto posId = button.positionChanged.connect([](int x, int y) {
        std::cout << "Button moved to (" << x << ", " << y << ")" << std::endl;
    });

    button.click();
    button.moveTo(100, 200);

    button.positionChanged.disconnect(posId);
    button.moveTo(150, 250);  // No output
}

Pattern Comparison

Pattern Purpose When to Use
Singleton Single instance, global access Loggers, config managers, hardware interfaces
Factory Object creation abstraction Multiple device types, plugin systems
Observer Event notification Interrupts, UI updates, data changes

Best Practices

Singleton

  • Use Meyer's Singleton for thread safety
  • Consider dependency injection as an alternative
  • Avoid overuse - can lead to hidden dependencies

Factory

  • Use when object creation is complex
  • Prefer Abstract Factory for families of related objects
  • Consider registration-based factory for extensibility

Observer

  • Use weak references to prevent memory leaks
  • Consider thread safety for multi-threaded applications
  • Prefer signals/slots for type-safe event handling


References

  • Design Patterns: Elements of Reusable Object-Oriented Software (GoF)
  • Modern C++ Design by Andrei Alexandrescu
  • cppreference.com