Skip to main content

    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

    💡 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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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().

    Try it Yourself »
    C++
    #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 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

    PatternUse it when…Modern C++ idiom
    SingletonYou need exactly one shared instance (logger, config)static local in instance()
    FactoryCreation should be decoupled from useunique_ptr + make_unique
    StrategyAn algorithm must be swappable at runtimestd::function + lambda
    ObserverMany objects react to one object's changesvector of std::function
    RAIIA resource must be released no matter whatctor 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.

    Try it Yourself »
    C++
    #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::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
    • 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service