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.