Design Patterns in C++ — Singleton, Factory, Observer, Strategy, CRTP, Policy-Based Design
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
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.
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).What replaces the classic observer pattern in modern C++? Answer:
std::functioncallbacks and lambdas, stored in a vector, eliminating the need for abstract observer interfaces.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.
When should you use
std::make_uniquein a factory? Answer: Always. It provides strong exception safety and is more concise thannew+unique_ptrconstructor.
FAQ
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