Skip to content

Design Patterns in C++ — Singleton, Factory, Observer, Strategy, CRTP, Policy-Based Design

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Design Patterns in C++. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ design patterns incorporate RAII, templates, and value semantics — the Singleton pattern uses Meyers' singleton, Factory uses make_unique, and CRTP (Curiously Recurring Template Pattern) provides compile-time polymorphism.

What You'll Learn

You will implement the Meyers singleton using local static variables, create abstract factories with std::unique_ptr, use CRTP for static polymorphism (compile-time virtual-like behavior), apply the observer pattern with std::function callbacks, use policy-based design with template parameters, and understand when C++ idioms replace traditional GoF patterns.

Why It Matters

Design patterns from the 1990s assumed languages without templates, RAII, or lambdas. Modern C++ replaces many GoF patterns with simpler, more efficient constructs. Understanding which patterns translate directly, which need adaptation, and which are obsolete is essential for writing idiomatic C++. The STL itself embodies many patterns (strategy via allocators, iterator via adaptors).

Learning Path

graph LR
    A["60: RAII & Resource Management"] --> B["61: Design Patterns in C++"]
    B --> C["62: Concurrency & Threads"]
    C --> D["63: Atomics & Synchronization"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Singleton — Meyers Singleton

The classic singleton is thread-safe and lazy-initialized using local static.

#include <iostream>
#include <mutex>

class Logger {
    std::mutex mutex_;
    Logger() = default;

public:
    // Meyers singleton: thread-safe, lazy, no dynamic allocation
    static Logger& instance() {
        static Logger logger;  // Initialized on first call (C++11 thread-safe)
        return logger;
    }

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

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
};

int main() {
    Logger::instance().log("Application started");
    Logger::instance().log("Singleton is thread-safe");

    // Same instance
    auto& logger1 = Logger::instance();
    auto& logger2 = Logger::instance();
    std::cout << "Same instance: " << (&logger1 == &logger2) << "\n";  // 1
}

Factory Method with unique_ptr

C++ factories return smart pointers for automatic memory management.

#include <iostream>
#include <memory>
#include <string>
#include <map>
#include <functional>

// Product hierarchy
class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
};

class Circle : public Shape {
public:
    void draw() const override { std::cout << "  ( )  Circle\n"; }
};

class Square : public Shape {
public:
    void draw() const override { std::cout << "  []  Square\n"; }
};

class Triangle : public Shape {
public:
    void draw() const override { std::cout << "  /\\  Triangle\n"; }
};

// Factory returning unique_ptr
class ShapeFactory {
    using Creator = std::function<std::unique_ptr<Shape>()>;
    std::map<std::string, Creator> creators_;

public:
    ShapeFactory() {
        registerType("circle", []() { return std::make_unique<Circle>(); });
        registerType("square", []() { return std::make_unique<Square>(); });
        registerType("triangle", []() { return std::make_unique<Triangle>(); });
    }

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

    std::unique_ptr<Shape> create(const std::string& type) const {
        auto it = creators_.find(type);
        if (it != creators_.end()) {
            return it->second();
        }
        return nullptr;
    }
};

int main() {
    ShapeFactory factory;

    auto shape1 = factory.create("circle");
    auto shape2 = factory.create("square");
    auto shape3 = factory.create("triangle");
    auto shape4 = factory.create("hexagon");  // nullptr

    if (shape1) shape1->draw();  // Circle
    if (shape2) shape2->draw();  // Square
    if (shape3) shape3->draw();  // Triangle
    if (!shape4) std::cout << "Hexagon not registered\n";
}

Observer Pattern with std::function

The observer pattern uses callbacks instead of abstract observer interfaces.

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

class Observable {
    std::vector<std::function<void(const std::string&)>> observers_;
public:
    // Subscribe with any callable
    template <typename Callable>
    void subscribe(Callable&& callback) {
        observers_.push_back(std::forward<Callable>(callback));
    }

    void notify(const std::string& event) {
        for (const auto& observer : observers_) {
            observer(event);
        }
    }

    // Unsubscribe: remove all matching callbacks (simplified)
    void clear() { observers_.clear(); }
};

int main() {
    Observable button;

    // Subscribe with lambdas
    button.subscribe([](const std::string& event) {
        std::cout << "Logger: " << event << "\n";
    });

    int clickCount = 0;
    button.subscribe([&clickCount](const std::string& event) {
        ++clickCount;
        std::cout << "Counter: " << clickCount << " events\n";
    });

    // Trigger events
    button.notify("click");
    button.notify("double_click");

    std::cout << "Total clicks: " << clickCount << "\n";

    // Subscribe with a member function
    struct Subscriber {
        void onEvent(const std::string& e) {
            std::cout << "Subscriber received: " << e << "\n";
        }
    };

    Subscriber sub;
    button.subscribe([&sub](const std::string& e) {
        sub.onEvent(e);
    });

    button.notify("hover");
}

CRTP — Static Polymorphism

The Curiously Recurring Template Pattern provides compile-time virtual dispatch without vtable overhead.

#include <iostream>
#include <type_traits>

// CRTP base class
template <typename Derived>
class ShapeBase {
public:
    // Static polymorphism: call derived's implementation
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }

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

    // No virtual functions needed
};

class Rectangle : public ShapeBase<Rectangle> {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}

    // Must provide areaImpl
    double areaImpl() const {
        return width_ * height_;
    }
};

class CircleShape : public ShapeBase<CircleShape> {
    double radius_;
public:
    explicit CircleShape(double r) : radius_(r) {}

    double areaImpl() const {
        return 3.14159 * radius_ * radius_;
    }
};

// Compile-time polymorphic function
template <typename T>
void processShape(const ShapeBase<T>& shape) {
    shape.print();  // Inlined! No vtable overhead
}

int main() {
    Rectangle rect(3.0, 4.0);
    CircleShape circ(5.0);

    rect.print();   // Area: 12
    circ.print();   // Area: 78.5397

    // Template function works with any CRTP-derived type
    processShape(rect);
    processShape(circ);
}

Policy-Based Design

Template parameters can specify behavior policies, replacing the Strategy Pattern at compile time.

#include <iostream>
#include <type_traits>

// Threading policies
struct SingleThreaded {
    void lock() const {}
    void unlock() const {}
};

struct MultiThreaded {
    mutable std::mutex mutex_;
    void lock() const { mutex_.lock(); }
    void unlock() const { mutex_.unlock(); }
};

// Locking policy
template <typename T, typename ThreadingPolicy = SingleThreaded>
class ThreadSafeValue : private ThreadingPolicy {
    T value_;
public:
    explicit ThreadSafeValue(T v) : value_(v) {}

    T get() const {
        this->lock();
        T result = value_;
        this->unlock();
        return result;
    }

    void set(T v) {
        this->lock();
        value_ = v;
        this->unlock();
    }
};

// Storage policies
struct HeapStorage {
    static void* allocate(size_t size) {
        void* p = std::malloc(size);
        std::cout << "Allocated " << size << " bytes on heap\n";
        return p;
    }
    static void deallocate(void* p) {
        std::free(p);
        std::cout << "Freed heap memory\n";
    }
};

struct StackStorage {
    static void* allocate(size_t size) {
        std::cout << "Using stack buffer (" << size << " bytes)\n";
        return std::alloca(size);  // Warning: alloca
    }
    static void deallocate(void*) {
        // Stack memory auto-freed
    }
};

int main() {
    // Single-threaded version (no mutex overhead)
    ThreadSafeValue<int, SingleThreaded> counter(0);
    counter.set(42);
    std::cout << "Counter: " << counter.get() << "\n";  // 42

    // Multi-threaded version (mutex protection)
    ThreadSafeValue<double, MultiThreaded> safePi(3.14);
    safePi.set(3.14159);
    std::cout << "Pi: " << safePi.get() << "\n";  // 3.14159
}

Strategy with std::function

The strategy pattern becomes trivial with std::function.

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

class Sorter {
    std::function<bool(int, int)> comparator_;
public:
    explicit Sorter(std::function<bool(int, int)> comp)
        : comparator_(std::move(comp)) {}

    void sort(std::vector<int>& data) const {
        std::sort(data.begin(), data.end(), comparator_);
    }

    void setComparator(std::function<bool(int, int)> comp) {
        comparator_ = std::move(comp);
    }
};

int main() {
    std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};

    Sorter sorter([](int a, int b) { return a < b; });
    sorter.sort(data);
    for (int x : data) std::cout << x << " ";  // 1 1 2 3 4 5 6 9
    std::cout << "\n";

    // Change strategy at runtime
    sorter.setComparator([](int a, int b) { return a > b; });
    sorter.sort(data);
    for (int x : data) std::cout << x << " ";  // 9 6 5 4 3 2 1 1
    std::cout << "\n";

    // Lambda with state
    int modulus = 3;
    sorter.setComparator([modulus](int a, int b) {
        return (a % modulus) < (b % modulus);
    });
    sorter.sort(data);
    for (int x : data) std::cout << x << " ";
    std::cout << "\n";  // Sort by remainder when divided by 3
}

Common Mistakes

Mistake 1: Overusing Singleton

Singletons introduce global state and testing difficulties. Use Dependency Injection or local instances.

Mistake 2: CRTP in headers without inline

CRTP is typically defined in headers. Ensure member functions are defined inline to avoid ODR violations.

Mistake 3: Dynamic allocation in factories when not needed

// Prefer:
auto makeShape() { return std::make_unique<Circle>(); }
// Over:
Shape* makeShape() { return new Circle(); }

Mistake 4: Using GoF patterns blindly without C++ modifications

Observer with virtual Notify is obsolete; use std::function callbacks. Strategy with abstract interfaces is verbose; use templates or std::function.

Mistake 5: Policy-based design with too many template parameters

template <typename T, typename Policy1, typename Policy2, typename Policy3>
class Widget;  // 3+ policies becomes unreadable

Use named template parameters with default arguments.

Practice Questions

  1. What is the Meyers singleton pattern? Answer: A local static variable in a static member function. Thread-safe (C++11 guarantees), lazy, no dynamic allocation.

  2. How does CRTP implement compile-time polymorphism? Answer: A base class template takes the derived class as its template parameter and calls derived's methods via static_cast<const Derived*>(this).

  3. What replaces the classic observer pattern in modern C++? Answer: std::function callbacks and lambdas, stored in a vector, eliminating the need for abstract observer interfaces.

  4. What is policy-based design? Answer: Template parameters specify behavior policies (e.g., threading model, allocation strategy). The compiler generates specialized code for each combination.

  5. When should you use std::make_unique in a factory? Answer: Always. It provides strong exception safety and is more concise than new + unique_ptr constructor.

FAQ

What design patterns are common in C++

Common patterns include: CRTP (static polymorphism), Meyers singleton, factory with unique_ptr, observer with std::function, and policy-based design via template parameters.

Does C++ use the Gang of Four patterns differently

Yes. RAII replaces many resource-management patterns. Templates replace interface-based polymorphism. std::function replaces observer and strategy abstract classes.

What is the CRTP pattern

The Curiously Recurring Template Pattern: a base class template takes the derived class as template parameter. It provides compile-time static polymorphism without virtual function overhead.

Is Singleton still useful in C++

In moderation. Meyers singleton is fine for logging, configuration, and resource pools. Avoid it for business logic that should be testable with dependency injection.

How do C++ templates replace the Strategy pattern

Template parameters act as compile-time strategies. The compiler generates specialized code for different strategies with zero runtime overhead — better than runtime polymorphism.

Mini Project

Implement a CRTP-based Comparable mixin that provides !=, <=, >, >= operators given only == and <:

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

// Your CRTP Comparable base

class Person : public Comparable<Person> {
public:
    std::string name;
    int age;

    Person(std::string n, int a) : name(std::move(n)), age(a) {}

    // Only need to provide these two:
    bool operator==(const Person& other) const { return age == other.age; }
    bool operator<(const Person& other) const { return age < other.age; }
};

int main() {
    Person alice("Alice", 30);
    Person bob("Bob", 25);
    Person charlie("Charlie", 30);

    std::cout << std::boolalpha;
    std::cout << "Alice > Bob: " << (alice > bob) << "\n";     // true
    std::cout << "Alice < Bob: " << (alice < bob) << "\n";     // false
    std::cout << "Alice >= Charlie: " << (alice >= charlie) << "\n";  // true
    std::cout << "Alice != Bob: " << (alice != bob) << "\n";   // true
    std::cout << "Alice != Charlie: " << (alice != charlie) << "\n"; // false

    // Can now sort with default comparator
    std::vector<Person> people = {alice, bob, charlie};
    std::sort(people.begin(), people.end());
    for (const auto& p : people) {
        std::cout << p.name << " ";
    }
    std::cout << "\n";  // Bob Alice Charlie (sorted by age)
}

This project demonstrates how C++ CRTP enables mixin-like behavior, similar to {{< ilink "Java" >} default interface methods but with no virtual dispatch overhead.

What's Next

You now understand how C++ design patterns leverage templates, RAII, and value semantics. Next, you will learn concurrency and threading — using std::thread, std::async, and synchronization primitives for multithreaded programming.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro