Advanced Track • Modern C++
Design Patterns in Modern C++
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.
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
unique_ptr or = 0 pure-virtual methods feel hazy, revisit Smart Pointers and Exception Safety first. Everything else, we'll build here.💡 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.
Worked example: Meyer's Singleton
Run it and confirm the Logger is created only once and both references share one object.
#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
}
publ
...2. 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.
Worked example: a shape factory
Notice main() only ever names Shape, never Circle or Square.
#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; // virt
...Your 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.
🎯 Your turn: finish the animal factory
Fill in the ___ blanks with make_unique, then check the output.
#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", r
...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.
Worked example: swappable sort strategies
The Sorter never changes — only the std::function plugged into it does.
#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(),
...Now 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:
🎯 Your turn: write two discount strategies
Write each pricing rule as a lambda, then check the totals match.
#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
Di
...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.
Worked example: a price feed with observers
Two lambdas subscribe; both run on every price change.
#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) {
...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.
Worked example: RAII with a FileGuard
The file 'closes' automatically on scope exit — you never call close().
#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 " << na
...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 adeletethey will eventually forget — a leak. Fix: returnunique_ptr<Shape>viamake_uniqueso 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 withoutvirtual ~Shape()is undefined behaviour and leaks the derived part. Fix: always give a polymorphic base avirtual(or= defaultvirtual) 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 |
Frequently Asked Questions
Q: 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.
Q: 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.
Q: 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.
Q: 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.
Q: 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.
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.
🎯 Mini-Challenge: build a notifier factory
Write the two notifier classes and the factory function yourself.
#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
...🎉 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::functionand swap algorithms (functions or lambdas) at runtime - ✅ Observer: keep a list of
std::functionsubscribers and notify them on change - ✅ RAII: acquire in the constructor, release in the destructor — automatic, exception-safe cleanup
- ✅ Prefer smart pointers and
std::functionover raw owning pointers and fat interfaces - ✅ Next lesson: Modular Applications — split these patterns across headers and source files
Sign up for free to track which lessons you've completed and get learning reminders.