Design Patterns
Reviewed & published by Brayan K
By the end of this lesson you'll be able to recognise five classic design patterns — Singleton, Factory, Strategy, Observer, and RAII — and write each one idiomatically in modern C++ using smart pointers, lambdas, and std::function instead of leak-prone raw pointers.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Write a thread-safe Singleton with a static local (and know when NOT to)
- Build a Factory that returns unique_ptr so objects free themselves
- Swap algorithms at runtime with the Strategy pattern via std::function
- Wire up Observer-style event notification with lambdas
- Use RAII so every resource is released automatically and exception-safely
- Choose between std::function and a virtual interface for a behaviour
💡 Real-World Analogy
Design patterns are like standard fittings in plumbing. A plumber doesn't invent a new joint for every house — they reach for a known fitting (a T-junction, a valve, a trap) that solves a recurring problem in a way the next plumber will instantly recognise. A Factory is the valve that decides what flows out; a Strategy is the swappable nozzle; an Observer is the alarm that triggers when pressure changes; RAII is the automatic shut-off that closes the supply the moment you walk away. Learning the patterns means you stop reinventing fittings and start speaking a shared language with every C++ developer who reads your code.
1. Singleton — Exactly One, Reachable Anywhere
The Singleton guarantees a class has exactly one instance and gives global access to it. In C++11 and later the cleanest version is Meyer's Singleton: a static local variable inside a function. The compiler guarantees it is created once, on first use, and that creation is thread-safe. You make the constructor private and = delete the copy operations so nobody can clone it. Read this worked example, run it, and watch the constructor fire only once.
Use sparingly: a Singleton is global state in disguise. It makes code harder to test and creates hidden coupling. Reach for it only for truly single resources like a logger — otherwise prefer passing the object in as a parameter.
#include <iostream>
#include <string>
using namespace std;
// A "pattern" is just a named, reusable shape for a common problem.
// SINGLETON = "there must be exactly ONE of this, reachable from anywhere."
// Meyer's Singleton: the modern, thread-safe C++ way.
class Logger {
string logFile;
// Private constructor -> nobody outside can write "Logger l;".
Logger(const string& file) : logFile(file) {
cout << "Logger created for: " << file << endl; // runs ONCE only
}
public:
// Delete copy + assignment so you can never duplicate the instance.
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static Logger& instance() {
// 'static' local: created the FIRST time this runs, kept forever.
// C++11 guarantees this initialisation is thread-safe.
static Logger inst("app.log");
return inst; // always the SAME object
}
void log(const string& msg) {
cout << "[LOG -> " << logFile << "] " << msg << endl;
}
};
int main() {
Logger::instance().log("Application started"); // builds Logger, then logs
Logger::instance().log("Processing data..."); // reuses the same Logger
Logger& a = Logger::instance();
Logger& b = Logger::instance();
// Both names point at one object, so their addresses are equal.
cout << "Same instance? " << (&a == &b ? "yes" : "no") << endl; // yes
return 0;
}
// ✅ Expected output:
// Logger created for: app.log
// [LOG -> app.log] Application started
// [LOG -> app.log] Processing data...
// Same instance? yes2. Factory — Create Without Naming the Type
The Factory pattern decouples creating an object from using it. Callers ask for something by name and get back a base-class pointer — here unique_ptr<Shape> — without ever mentioning the concrete class. Returning a unique_ptr is the key modern detail: the object owns itself and frees automatically, so there's no delete to forget. Read the worked example first.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
// FACTORY = "ask for a thing by name; get back the right object, hidden behind
// a base-class pointer." The caller never names the concrete class.
// Abstract base ("interface"): pure virtual -> Shape cannot be built directly.
class Shape {
public:
virtual void draw() const = 0; // = 0 means "subclasses MUST define"
virtual double area() const = 0;
virtual ~Shape() = default; // virtual dtor -> safe to delete via base*
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
void draw() const override { cout << "Circle r=" << radius << endl; }
double area() const override { return 3.14159 * radius * radius; }
};
class Square : public Shape {
double side;
public:
explicit Square(double s) : side(s) {}
void draw() const override { cout << "Square s=" << side << endl; }
double area() const override { return side * side; }
};
// The factory returns unique_ptr<Shape>: the object OWNS itself and is freed
// automatically. No 'new', no 'delete', no leaks.
unique_ptr<Shape> createShape(const string& type, double size) {
if (type == "circle") return make_unique<Circle>(size);
if (type == "square") return make_unique<Square>(size);
throw invalid_argument("Unknown shape: " + type);
}
int main() {
// Code below depends only on Shape, never on Circle/Square.
auto a = createShape("circle", 5.0);
auto b = createShape("square", 4.0);
a->draw(); cout << " area = " << a->area() << endl; // Circle r=5 ... 78.5
b->draw(); cout << " area = " << b->area() << endl; // Square s=4 ... 16
return 0; // a and b free themselves here
}
// ✅ Expected output:
// Circle r=5
// area = 78.5397
// Square s=4
// area = 16Your turn. The factory below is almost complete — fill in the two blanks marked ___ so each branch returns the right animal on the heap, using the // 👉 hints.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Animal {
public:
virtual string speak() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal { public: string speak() const override { return "Woof"; } };
class Cat : public Animal { public: string speak() const override { return "Meow"; } };
unique_ptr<Animal> createAnimal(const string& kind) {
// 🎯 YOUR TURN — make the factory return the right animal.
// 1) If kind == "dog", return a Dog made on the heap.
if (kind == "dog") return ___; // 👉 make_unique<Dog>()
// 2) If kind == "cat", return a Cat made on the heap.
if (kind == "cat") return ___; // 👉 make_unique<Cat>()
throw invalid_argument("Unknown: " + kind);
}
int main() {
auto pet = createAnimal("dog");
cout << pet->speak() << endl;
// ✅ Expected output:
// Woof
return 0;
}3. Strategy — Swap the Algorithm at Runtime
The Strategy pattern wraps an algorithm behind a common interface so you can change it on the fly. Classic C++ used an abstract base class with one virtual method; modern C++ usually skips that and stores a std::function instead — then any callable with the right signature (a free function, a lambda, even a captured object) is a valid strategy. Read the worked example, then write your own strategies.
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
// STRATEGY = "swap the ALGORITHM at runtime without changing the caller."
// In modern C++ the strategy is just a std::function -> any matching callable.
using SortStrategy = function<void(vector<int>&)>;
void ascending(vector<int>& v) {
cout << "Sorting ascending\n";
sort(v.begin(), v.end());
}
void descending(vector<int>& v) {
cout << "Sorting descending\n";
sort(v.begin(), v.end(), greater<int>());
}
class Sorter {
SortStrategy strategy; // holds whatever you plug in
public:
void setStrategy(SortStrategy s) { strategy = s; }
void run(vector<int>& data) { if (strategy) strategy(data); }
};
void print(const vector<int>& v) {
for (int x : v) cout << x << ' ';
cout << '\n';
}
int main() {
Sorter sorter;
vector<int> data = {42, 17, 93, 5};
sorter.setStrategy(ascending); // plug in one algorithm
auto up = data; sorter.run(up); print(up); // 5 17 42 93
sorter.setStrategy(descending); // swap it for another
auto down = data; sorter.run(down); print(down); // 93 42 17 5
// A lambda is a valid strategy too — no named function needed:
sorter.setStrategy([](vector<int>& v){ v.clear(); cout << "Cleared\n"; });
auto gone = data; sorter.run(gone); print(gone); // (empty line)
return 0;
}
// ✅ Expected output:
// Sorting ascending
// 5 17 42 93
// Sorting descending
// 93 42 17 5
// ClearedNow you try. A Discount is a strategy that takes a price and returns a new price. Fill in the two blanks with lambdas using the hints:
#include <iostream>
#include <functional>
using namespace std;
// A discount strategy takes a price and returns the new price.
using Discount = function<double(double)>;
double checkout(double price, Discount apply) {
return apply(price);
}
int main() {
// 🎯 YOUR TURN — write two pricing strategies as lambdas.
// 1) "tenPercentOff": return price * 0.9
Discount tenPercentOff = ___; // 👉 [](double p){ return p * 0.9; }
// 2) "fiverOff": subtract 5 from the price
Discount fiverOff = ___; // 👉 [](double p){ return p - 5; }
cout << "Sale: " << checkout(50.0, tenPercentOff) << endl;
cout << "Coupon: " << checkout(50.0, fiverOff) << endl;
// ✅ Expected output:
// Sale: 45
// Coupon: 45
return 0;
}4. Observer — Tell Everyone Who Signed Up
The Observer pattern lets objects subscribe to changes in another object without the two being tightly coupled. The subject (here a PriceFeed) keeps a list of subscribers and notifies each one when something changes. Storing subscribers as std::function means each observer is just a lambda — no observer base class required.
Pro Tip: for thread-safe observers, guard the listener list with a std::mutex, and if observers can outlive or die before the subject, store std::weak_ptr so you never call into a destroyed object.
#include <iostream>
#include <vector>
#include <string>
#include <functional>
using namespace std;
// OBSERVER = "when something changes, tell everyone who signed up — without the
// subject knowing who they are." Modern C++ stores subscribers as std::function.
class PriceFeed {
using Listener = function<void(double)>;
vector<Listener> listeners; // the "observers"
public:
void subscribe(Listener cb) { listeners.push_back(cb); }
void setPrice(double price) {
cout << "Price now $" << price << '\n';
for (auto& notify : listeners) notify(price); // tell every observer
}
};
int main() {
PriceFeed feed;
// The dashboard just prints the new price.
feed.subscribe([](double p){ cout << " Dashboard: $" << p << '\n'; });
// The alert only reacts above a threshold.
feed.subscribe([](double p){
if (p > 100) cout << " ALERT: above $100!\n";
});
feed.setPrice(85); // both observers run; alert stays quiet
feed.setPrice(120); // both run; alert fires
return 0;
}
// ✅ Expected output:
// Price now $85
// Dashboard: $85
// Price now $120
// Dashboard: $120
// ALERT: above $100!5. RAII — The Pattern That Powers the Rest
RAII (Resource Acquisition Is Initialisation) is the most important pattern in C++, and it's the one beginners overlook because it has no Gang-of-Four name. The idea: tie a resource's lifetime to an object. Acquire it in the constructor, release it in the destructor, and the language guarantees the destructor runs when the object leaves scope — even if an exception is thrown. Smart pointers are RAII for memory; the same pattern handles files, locks, and sockets.
#include <iostream>
#include <string>
using namespace std;
// RAII = "Resource Acquisition Is Initialisation." The pattern: tie a resource's
// lifetime to an object. Acquire in the constructor, release in the destructor.
// When the object leaves scope, cleanup happens AUTOMATICALLY — even if an
// exception is thrown. This is the most important "pattern" in all of C++.
class FileGuard {
string name;
public:
explicit FileGuard(const string& n) : name(n) {
cout << "OPEN " << name << '\n'; // acquire the resource
}
~FileGuard() {
cout << "CLOSE " << name << '\n'; // release it, guaranteed
}
};
void process() {
FileGuard f("data.txt"); // OPEN happens here
cout << " ...working with data.txt\n";
} // CLOSE happens HERE, on scope exit
int main() {
process();
cout << "Done — file was closed for us.\n";
// Smart pointers ARE RAII: unique_ptr frees memory in its destructor the
// exact same way FileGuard closes the file.
return 0;
}
// ✅ Expected output:
// OPEN data.txt
// ...working with data.txt
// CLOSE data.txt
// Done — file was closed for us.Common Errors (and the fix)
- Singleton overuse: reaching for a Singleton whenever something feels "global". It hides state and breaks tests because you can't substitute a fake. Fix: pass the dependency in as a parameter; keep Singletons for one or two truly global services.
- Returning a raw owning pointer: a factory that does return new Circle(r); hands the caller a delete they will eventually forget — a leak. Fix: return unique_ptr<Shape> via make_unique so ownership is explicit and cleanup automatic.
- Fat interfaces: one base class with a dozen unrelated pure-virtual methods forces empty or throw-only overrides. Fix (Interface Segregation): split it into small, focused interfaces so each class implements only what it uses.
- "error: call to deleted constructor": you tried to copy a Singleton (e.g. Logger l = Logger::instance();). The copy was deleted on purpose. Fix: take a reference — Logger& l = Logger::instance();.
- Missing virtual destructor: deleting a derived object through a Shape* base pointer without virtual ~Shape() is undefined behaviour and leaks the derived part. Fix: always give a polymorphic base a virtual (or = default virtual) destructor.
📋 Quick Reference
| Pattern | Use it when… | Modern C++ idiom |
|---|---|---|
| Singleton | You need exactly one shared instance (logger, config) | static local in instance() |
| Factory | Creation should be decoupled from use | unique_ptr + make_unique |
| Strategy | An algorithm must be swappable at runtime | std::function + lambda |
| Observer | Many objects react to one object's changes | vector of std::function |
| RAII | A resource must be released no matter what | ctor acquires / dtor releases |
Mini-Challenge: a Notifier Factory
No blanks this time — just a brief and an outline. Combine the Factory pattern with a polymorphic interface to build it from scratch, then run it and check your output against the example in the comments.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Notifier {
public:
virtual void send(const string& msg) const = 0;
virtual ~Notifier() = default;
};
int main() {
// 🎯 MINI-CHALLENGE: a notifier Factory
// 1. Write two classes that inherit Notifier: EmailNotifier and SmsNotifier.
// Each overrides send() to print e.g. "EMAIL: <msg>" / "SMS: <msg>".
// 2. Write unique_ptr<Notifier> makeNotifier(const string& kind)
// that returns the right one ("email" / "sms"), like createShape() earlier.
// 3. In main, make an "email" notifier and call send("Order shipped").
//
// ✅ Example output (kind = "email"):
// EMAIL: Order shipped
// your code here
return 0;
}🎉 Lesson Complete
- ✅ Singleton: a static local in instance() gives one thread-safe object — use it sparingly
- ✅ Factory: return unique_ptr<Base> so callers stay decoupled and nothing leaks
- ✅ Strategy: store a std::function and swap algorithms (functions or lambdas) at runtime
- ✅ Observer: keep a list of std::function subscribers and notify them on change
- ✅ RAII: acquire in the constructor, release in the destructor — automatic, exception-safe cleanup
- ✅ Prefer smart pointers and std::function over raw owning pointers and fat interfaces
Practice quiz
What does the Singleton pattern guarantee?
- A class can never be instantiated
- Every object is automatically copied
- A class has exactly one instance, reachable from anywhere
- A class always returns a new object on each call
Answer: A class has exactly one instance, reachable from anywhere. Singleton ensures exactly one instance with global access — useful for a logger or config, but use it sparingly.
How does Meyer's Singleton create its single, thread-safe instance in modern C++?
- A static local variable inside a function (instance())
- A global variable at file scope
- A new call in the constructor
- A shared_ptr passed everywhere
Answer: A static local variable inside a function (instance()). A static local inside instance() is created once on first use, and C++11 guarantees that initialisation is thread-safe.
Why does a modern Factory return unique_ptr<Shape> instead of a raw pointer?
- Raw pointers cannot point to derived classes
- unique_ptr is faster to construct
- It is required by the Shape base class
- unique_ptr owns the object and frees it automatically, so there is no delete to forget (no leaks)
Answer: unique_ptr owns the object and frees it automatically, so there is no delete to forget (no leaks). Returning unique_ptr makes ownership explicit and cleanup automatic, so a forgotten delete can never leak.
In modern C++, the Strategy pattern is usually implemented by storing what?
- A raw function pointer array
- A std::function that holds any matching callable
- A global flag
- A vector of integers
Answer: A std::function that holds any matching callable. Strategy stores a std::function, so any matching callable — a free function, lambda, or functor — is a valid strategy.
What does the Observer pattern let you do?
- Notify all subscribers when a subject changes, without the subject knowing who they are
- Guarantee one instance of a class
- Swap an algorithm at runtime
- Free memory automatically
Answer: Notify all subscribers when a subject changes, without the subject knowing who they are. Observer keeps a list of subscribers (often std::function) and notifies each one on a change, keeping them decoupled.
What is the core idea of RAII?
- Allocate all memory at program start
- Never use destructors
- Tie a resource's lifetime to an object: acquire in the constructor, release in the destructor
- Always call close() manually
Answer: Tie a resource's lifetime to an object: acquire in the constructor, release in the destructor. RAII acquires a resource in the constructor and releases it in the destructor, so cleanup is automatic and exception-safe.
Why must a polymorphic base class like Shape have a virtual destructor?
- To make the class abstract
- Deleting a derived object through a base pointer without it is undefined behaviour and leaks the derived part
- To allow copying
- Virtual destructors are never needed
Answer: Deleting a derived object through a base pointer without it is undefined behaviour and leaks the derived part. Without virtual ~Shape(), deleting a derived object via Shape* is undefined behaviour and the derived part leaks.
How do you stop a Singleton from being duplicated?
- Make the constructor public
- Mark the class final
- Use a global variable
- = delete the copy constructor and copy assignment operator
Answer: = delete the copy constructor and copy assignment operator. Deleting the copy constructor and assignment (and making the constructor private) ensures nobody can clone the instance.
When should you prefer std::function over a virtual interface for a behaviour?
- When the behaviour has many related methods and its own state
- When the strategy or callback is small and you want maximum flexibility with no class to write
- Never; interfaces are always better
- Only for memory management
Answer: When the strategy or callback is small and you want maximum flexibility with no class to write. std::function shines for small one-method strategies and callbacks; a virtual interface fits richer, multi-method contracts.
What is a 'fat interface' and how do you fix it?
- An interface with one method; merge it with others
- A class that uses too much memory; shrink its fields
- A base class with many unrelated pure-virtual methods; split it into small focused interfaces (Interface Segregation)
- An interface with a virtual destructor; remove it
Answer: A base class with many unrelated pure-virtual methods; split it into small focused interfaces (Interface Segregation). A fat interface forces empty or throw-only overrides; the fix is to split it into several small, focused interfaces.
Continue this course
- Previous: Exception Safety Levels (Basic, Strong, No-Throw Guarantees)
- Next: Building Modular Applications with Header/Implementation Architecture — Structure large C++ projects with headers, source files, and modules
- Quick reference: C++ cheat sheet
Frequently asked questions
Is the Singleton pattern an anti-pattern I should avoid?
Not always, but treat it with suspicion. A Singleton is hidden global state: it makes code harder to test (you cannot swap in a fake) and creates surprising coupling. Use it only for genuinely single, global resources like a logger or configuration store. If you find yourself reaching for it often, prefer passing the object in as a parameter (dependency injection) instead.
Why do the factory examples return unique_ptr instead of a raw pointer?
A raw owning pointer (returning 'new Circle(...)') makes the caller responsible for calling delete, and one forgotten delete is a memory leak. unique_ptr owns the object and frees it automatically when it goes out of scope, so leaks become impossible. Returning unique_ptr<Shape> is the modern idiom for factories.
When should I use std::function and when should I use a virtual interface?
Use std::function (with lambdas) when the strategy or callback is small and you want maximum flexibility — any matching callable works, with no class to write. Use a virtual interface when the behaviour has multiple related methods, needs its own state, or forms a stable contract many classes implement. std::function shines for one-method strategies and observers; interfaces shine for richer abstractions.
Do I still need RAII if I use smart pointers everywhere?
Smart pointers ARE RAII — unique_ptr and shared_ptr free memory in their destructors. But RAII applies to every resource, not just memory: open files, locked mutexes, network sockets, database handles. Wrapping each in a small RAII guard (like std::lock_guard for mutexes) means cleanup is automatic and exception-safe, with no manual close() to forget.
What is a 'fat interface' and why is it a problem?
A fat interface is a base class that piles on many unrelated pure-virtual methods, forcing every subclass to implement things it does not need. It leads to empty or throw-only overrides and brittle code. The fix is the Interface Segregation Principle: split it into several small, focused interfaces so a class only implements what it actually uses.