Skip to main content
C++ advanced Lesson 20 of 23

Design Patterns in C++

Implement CRTP, policy-based design, PIMPL idiom, Observer, and Factory patterns idiomatically in C++.

Design Patterns in C++

C++ supports a wider range of design pattern implementations than most languages. Beyond the classic OOP patterns (Observer, Factory, Singleton) that exist in Java and Python, C++ adds compile-time patterns like CRTP and policy-based design that have no equivalent in garbage-collected languages. These patterns achieve polymorphism and customization with zero runtime overhead by moving decisions to compile time. Knowing which tool fits which problem separates competent C++ from idiomatic C++.


CRTP: Curiously Recurring Template Pattern

CRTP achieves static polymorphism — polymorphic behavior resolved at compile time, with zero vtable cost. The pattern works by having the base class parameterized on the derived type, enabling it to call derived class methods via a static_cast. The compiler sees the full type at compile time, inlines the calls, and produces code as efficient as direct function calls.

#include <iostream>

// Base class parameterized on the derived type — the "curiously recurring" part
template <typename Derived>
class Shape {
public:
    // Static dispatch — no virtual keyword, no vtable, compiler can inline
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }

    void print() const {
        std::cout << "Area: " << area() << "\n";
    }
};

class Circle : public Shape<Circle> {
public:
    explicit Circle(double r) : radius(r) {}
    double areaImpl() const { return 3.14159 * radius * radius; }
private:
    double radius;
};

class Rectangle : public Shape<Rectangle> {
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double areaImpl() const { return width * height; }
private:
    double width, height;
};

int main() {
    Circle c(5.0);
    Rectangle r(3.0, 4.0);
    c.print();   // Area: 78.5398
    r.print();   // Area: 12
}

CRTP is also ideal for mixins — injecting reusable behavior into a class without virtual overhead. This example adds all comparison operators from just == and <:

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

class Point : public Comparable<Point> {
public:
    Point(int x, int y) : x(x), y(y) {}
    bool operator==(const Point& o) const { return x == o.x && y == o.y; }
    bool operator<(const Point& o) const {
        return x < o.x || (x == o.x && y < o.y);
    }
private:
    int x, y;
};

Policy-Based Design

Policies are template parameters that provide customizable behavior. They let you compose a class from orthogonal, interchangeable strategies at compile time — the resulting code has no virtual dispatch, no branching, and no overhead compared to writing the specific combination by hand. This is how the standard library achieves flexibility without sacrificing performance (allocators in containers, comparators in maps).

#include <iostream>
#include <mutex>

// Two interchangeable threading policies — same interface, different behavior
struct SingleThreaded {
    struct Lock { Lock(SingleThreaded&) {} };  // no-op lock
};

struct MultiThreaded {
    std::mutex mtx;
    struct Lock {
        std::unique_lock<std::mutex> lk;
        Lock(MultiThreaded& mt) : lk(mt.mtx) {}  // real mutex lock
    };
};

// Storage policy — can be swapped without changing Buffer's logic
template <typename T>
struct HeapStorage {
    T* allocate(size_t n) { return new T[n]; }
    void deallocate(T* p) { delete[] p; }
};

template <
    typename T,
    typename ThreadingPolicy = SingleThreaded,   // default: no locking overhead
    typename StoragePolicy   = HeapStorage<T>
>
class Buffer : private ThreadingPolicy, private StoragePolicy {
public:
    explicit Buffer(size_t size) : data(StoragePolicy::allocate(size)), sz(size) {}
    ~Buffer() { StoragePolicy::deallocate(data); }

    void write(size_t i, T val) {
        typename ThreadingPolicy::Lock lock(*this);  // no-op or real lock
        data[i] = val;
    }

    T read(size_t i) const { return data[i]; }

private:
    T*     data;
    size_t sz;
};

// Single-threaded buffer — zero overhead from locking
Buffer<int> local(1024);

// Thread-safe buffer — mutex included, same API
Buffer<int, MultiThreaded> shared(1024);

PIMPL Idiom

PIMPL (Pointer to IMPLementation) hides implementation details behind an opaque pointer. The public header only sees a forward declaration of the implementation struct — none of the private headers, heavy dependencies, or internal data types are visible to users. This has two important benefits: it reduces compilation time (changing private internals only recompiles the .cpp, not every consumer of the header), and it stabilizes the ABI (adding a private member doesn’t change the public struct’s size).

// widget.h — only this header is included by users
#pragma once
#include <memory>
#include <string>

class Widget {
public:
    explicit Widget(std::string name);
    ~Widget();                         // must be defined in .cpp where Impl is complete
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;

    void render();
    void setColor(int r, int g, int b);

private:
    struct Impl;                       // forward declaration — users never see the definition
    std::unique_ptr<Impl> pImpl;
};
// widget.cpp — heavy dependencies stay here, invisible to users of widget.h
#include "widget.h"
#include <iostream>
// #include <SomeHeavyLibrary.h>  // users never see this include

struct Widget::Impl {
    std::string name;
    int r = 0, g = 0, b = 0;

    explicit Impl(std::string n) : name(std::move(n)) {}

    void render() {
        std::cout << "Rendering " << name
                  << " rgb(" << r << "," << g << "," << b << ")\n";
    }
};

Widget::Widget(std::string name)
    : pImpl(std::make_unique<Impl>(std::move(name))) {}

Widget::~Widget() = default;   // unique_ptr needs complete Impl type here — must be in .cpp
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

void Widget::render()              { pImpl->render(); }
void Widget::setColor(int r, int g, int b) {
    pImpl->r = r; pImpl->g = g; pImpl->b = b;
}

Changing Impl internals only recompiles widget.cpp, not every file that includes widget.h. This dramatically speeds up large builds and stabilizes the ABI.


Observer Pattern

The Observer pattern decouples event producers from event consumers. The producer fires events; consumers subscribe to them. The modern C++ approach uses std::function instead of a virtual base class, making it flexible enough to subscribe lambdas, free functions, or member functions without a base class hierarchy.

#include <functional>
#include <vector>
#include <algorithm>
#include <iostream>

template <typename... Args>
class Event {
public:
    using Handler = std::function<void(Args...)>;
    using HandlerId = size_t;

    // Subscribe returns an ID so the subscriber can unsubscribe later
    HandlerId subscribe(Handler handler) {
        size_t id = nextId++;
        handlers.push_back({id, std::move(handler)});
        return id;
    }

    void unsubscribe(HandlerId id) {
        handlers.erase(
            std::remove_if(handlers.begin(), handlers.end(),
                [id](const auto& h) { return h.first == id; }),
            handlers.end());
    }

    void emit(Args... args) {
        for (auto& [id, handler] : handlers)
            handler(args...);
    }

private:
    std::vector<std::pair<HandlerId, Handler>> handlers;
    HandlerId nextId = 0;
};

// Usage — no virtual base class required
class Button {
public:
    Event<int, int> onClick;  // event carrying x, y coordinates
    void click(int x, int y) { onClick.emit(x, y); }
};

int main() {
    Button btn;
    auto id = btn.onClick.subscribe([](int x, int y) {
        std::cout << "Clicked at (" << x << ", " << y << ")\n";
    });
    btn.click(10, 20);       // Clicked at (10, 20)
    btn.onClick.unsubscribe(id);
    btn.click(30, 40);       // no output — handler was removed
}

Factory Pattern

The Factory pattern centralizes object creation, decoupling callers from concrete types. This is especially valuable when the concrete type is determined at runtime (from a config file, user input, or plugin system) and when you want to add new types without modifying existing code.

#include <memory>
#include <string>
#include <unordered_map>
#include <functional>
#include <stdexcept>

class Serializer {
public:
    virtual ~Serializer() = default;
    virtual std::string serialize(const std::string& data) = 0;

    // Static factory method — callers never mention concrete types
    static std::unique_ptr<Serializer> create(const std::string& format);
};

class JsonSerializer : public Serializer {
public:
    std::string serialize(const std::string& data) override {
        return "{\"data\": \"" + data + "\"}";
    }
};

class XmlSerializer : public Serializer {
public:
    std::string serialize(const std::string& data) override {
        return "<data>" + data + "</data>";
    }
};

// Self-registering factory — adding a new format only requires adding one entry here
std::unique_ptr<Serializer> Serializer::create(const std::string& format) {
    static const std::unordered_map<
        std::string,
        std::function<std::unique_ptr<Serializer>()>
    > registry = {
        {"json", [] { return std::make_unique<JsonSerializer>(); }},
        {"xml",  [] { return std::make_unique<XmlSerializer>(); }},
    };

    auto it = registry.find(format);
    if (it == registry.end())
        throw std::invalid_argument("Unknown format: " + format);
    return it->second();
}

int main() {
    auto s = Serializer::create("json");
    std::cout << s->serialize("hello") << "\n";
    // {"data": "hello"}
}

Thread-Safe Singleton

The Singleton pattern ensures a class has exactly one instance. The C++11 standard guarantees that local static initialization is thread-safe — the initialization happens exactly once, even if multiple threads race to use the instance for the first time. This eliminates the double-checked locking boilerplate that was necessary in C++03.

class Config {
public:
    // Meyers singleton — initialization is thread-safe since C++11, no locks needed
    static Config& instance() {
        static Config inst;   // initialized once, lazily, thread-safe
        return inst;
    }

    const std::string& get(const std::string& key) const {
        return settings.at(key);
    }

    void set(const std::string& key, std::string value) {
        settings[key] = std::move(value);
    }

    // Prevent copying — a singleton must not be duplicated
    Config(const Config&)            = delete;
    Config& operator=(const Config&) = delete;

private:
    Config() { settings["version"] = "1.0"; }  // private: only instance() can create it
    std::unordered_map<std::string, std::string> settings;
};

// Usage
Config::instance().set("theme", "dark");
auto theme = Config::instance().get("theme");

Prefer dependency injection over Singleton for testability. Use Singleton only for truly global, stateless (or carefully stateful) resources like logging or configuration.

Frequently Asked Questions

When should I use CRTP over virtual functions?
When you need compile-time polymorphism to avoid vtable overhead, or when you want to inherit implementation without runtime cost (e.g., SFINAE-free mixins).
What problem does PIMPL solve?
It hides implementation details from the header, reducing compilation dependencies and keeping the ABI stable when internals change.