Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 23 – Design, idioms and maintainable C++

Classic design patterns in modern C++

Strategy, observer, factory, command and visitor in modern C++: lambdas, smart pointers and std::variant where class hierarchies used to be.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Five classic patterns lose most of their classes in modern C++ · A strategy is a callable the caller passes in · A class hierarchy is still right for wide or open interfaces · Practise: patterns in their modern form. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Implement strategy, observer, factory, command and visitor in idiomatic modern C++
  • Replace class-heavy pattern code with lambdas, smart pointers and variants where that is simpler
  • Prevent the lifetime and re-entrancy bugs of observers and self-registering factories
  • Compare the memory cost of three ways to store commands for undo and redo
  • Choose between a callable, a class hierarchy and a variant for an interface

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement strategy, observer, factory, command and visitor in idiomatic modern C++
  • Replace class-heavy pattern code with lambdas, smart pointers and variants where that is simpler
  • Prevent the lifetime and re-entrancy bugs of observers and self-registering factories
  • Compare the memory cost of three ways to store commands for undo and redo
  • Choose between a callable, a class hierarchy and a variant for an interface

Before you start

On this page

One “order shipped” feature here needs five classic designs: telling every screen about the event, creating the customer’s channel by name, choosing how the message is delivered, keeping each send so that it can be undone, and pricing each kind of message. The classic design pattern: A named, reusable design for a problem that keeps coming back in object-oriented code, such as strategy, observer, factory, command or visitor, so that a whole design can be described in a word. In C++ the same idea often needs fewer classes than its classic form. are named, reusable designs for problems like these that keep coming back in object-oriented code, and in their classic form each one is a small hierarchy of classes.

Modern C++ keeps the ideas and drops most of the classes. A strategy becomes a callable, an observer’s list holds std::weak_ptr, a factory is a map from names to functions that return std::unique_ptr, a command is a value that carries enough data to undo itself, and a visitor over a closed set of types is a std::variant with std::visit. Every program below ran with Apple clang 21 and GCC 16.

Five classic patterns lose most of their classes in modern C++

The pattern in its classic form In modern C++
Strategy: an abstract class and a subclass per algorithm A callable: a lambda, a function or a template parameter
Observer: a list of pointers to subscribers A list of std::weak_ptr
Factory: a creator class per product A map from names to functions that return std::unique_ptr
Command: a command class per action A struct of data, or a std::variant of structs
Visitor: double dispatch through accept() std::variant and std::visit

Each pattern solves one problem. Strategy swaps one piece of behaviour, such as how a message is delivered, without changing the code that uses it. observer pattern: A design in which an event keeps a list of subscribers and tells each of them when it happens. In modern C++ the list holds std::weak_ptr, so a subscriber that has been destroyed is skipped instead of called. lets an event tell everyone who subscribed, and std::weak_ptr lets a destroyed subscriber be skipped instead of called. Factory creates an object of a class chosen at run time, by a name from a configuration file or a request. Command turns an action into an object that can be stored, queued and reversed, which is what undo and redo need. Visitor runs one operation over each type of a closed set of types: A set of types that one piece of code owns and knows completely, such as the shapes of one program or the messages of a fixed protocol. std::variant suits it: std::visit refuses a visitor that misses one of the types. More in “Alternatives to inheritance: variant, composition”, and with std::visit the compiler reports a type that has no handler. The diagram follows one notification through all five:

One notification feature from top to bottom: observer, factory, strategy, command and visitor, each with its modern C++ form.An order shipsObserverthe event tells every subscriber;subscribers held by std::weak_ptrFactorythe customer's channel is madefrom its name, "sms" or "push";a map of names to functionsthat return std::unique_ptrStrategythe channel delivers the message;a callable, such as a lambdaCommandeach send is kept as datathat can be undone or redoneVisitoreach kind of message is pricedand described; std::variantand std::visit

Five classic patterns in one notification feature

Text description of the diagram

The diagram is one column of six boxes joined by arrows, following one notification from top to bottom.

  1. An order ships.
  2. Observer: the event tells every subscriber; the subscribers are held by std::weak_ptr.
  3. Factory: the customer's channel is made from its name, "sms" or "push", by a map from names to functions that return std::unique_ptr.
  4. Strategy: the channel delivers the message; it is a callable, such as a lambda.
  5. Command: each send is kept as data that can be undone or redone.
  6. Visitor: each kind of message is priced and described, with std::variant and std::visit.

Checkpoint

Check: The modern form of each pattern

  1. Question 1 of 2 An event must reach every screen that subscribed, and a screen that has been closed must be skipped. Which pattern is that, in which modern form?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Observer, with the subscribers held by std::weak_ptr

    Observer lets an event tell everyone who subscribed. Holding the subscribers through std::weak_ptr lets a destroyed one be skipped instead of called.

  2. Question 2 of 2 A configuration file names the channel to create, "sms" or "push". Which modern form of which pattern fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A factory, as a map from names to functions that return std::unique_ptr

    A factory creates an object of a class chosen at run time, for example by a name from a configuration file. In modern C++ it is a map from names to functions that return std::unique_ptr.

A strategy is a callable the caller passes in

Notifier sends “order shipped” messages and does not know how they are delivered. Its strategy is a std::function<void(const Message&)>, so a plain function, a function object: An object of a class that defines operator(), so it can be called like a function and can carry state in its members, as a counter of calls. Every lambda is one, of a class the compiler writes. Also called a functor. More in “Function pointers, functors and std::function” with state of its own and a lambda all qualify, and each customer’s preference picks one at run time:

strategy.cpp C++ · strategy.cpp
// strategy.cpp: one notification feature. How a message is delivered is a strategy, and the strategy is just a
// callable: a function, a function object with state, or a lambda, chosen at run time.
#include <format>
#include <functional>
#include <map>
#include <print>
#include <string>
#include <utility>
#include <vector>

struct Message {
    std::string to;
    std::string text;
};

using Deliver = std::function<void(const Message&)>;    // the strategy: anything callable with a Message

class Notifier {
public:
    explicit Notifier(Deliver deliver) : deliver_(std::move(deliver)) {}
    void set_delivery(Deliver deliver) { deliver_ = std::move(deliver); }
    void order_shipped(const std::string& customer, int order) const {
        deliver_({customer, std::format("order {} has shipped", order)});
    }

private:
    Deliver deliver_;
};

void by_sms(const Message& m) { std::println("  SMS to {}: {}", m.to, m.text); }

struct ByEmail {                                         // a strategy with state of its own
    std::string sender;
    void operator()(const Message& m) const { std::println("  e-mail from {} to {}: {}", sender, m.to, m.text); }
};

int main() {
    const std::map<std::string, Deliver, std::less<>> channels{
        {"sms", by_sms},
        {"email", ByEmail{"orders@example.com"}},
        {"push", [](const Message& m) { std::println("  push to {}'s phone: {}", m.to, m.text); }},
    };
    const std::vector<std::pair<std::string, std::string>> customers{{"Asha", "sms"}, {"Ravi", "push"}, {"Meera", "email"}};

    Notifier notifier{channels.at("sms")};
    int order = 1001;
    for (const auto& [name, preference] : customers) {
        notifier.set_delivery(channels.at(preference));   // the customer's preference picks the strategy
        notifier.order_shipped(name, order++);
    }

    std::vector<Message> sent;                            // in a test: a strategy that records instead of sending
    const Notifier under_test{[&sent](const Message& m) { sent.push_back(m); }};
    under_test.order_shipped("Kabir", 2001);
    std::println("the test strategy recorded {} message: \"{}\"", sent.size(), sent.at(0).text);
}
Three strategies chosen at run time, and one for tests Python · strategy_session.py
from session import run

# Three delivery strategies chosen at run time, and one for tests, with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra strategy.cpp -o strategy && ./strategy
g++-16 -std=c++23 -Wall -Wextra strategy.cpp -o strategy-gcc && ./strategy-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra strategy.cpp -o strategy && ./strategy
  SMS to Asha: order 1001 has shipped
  push to Ravi's phone: order 1002 has shipped
  e-mail from orders@example.com to Meera: order 1003 has shipped
the test strategy recorded 1 message: "order 2001 has shipped"
$ g++-16 -std=c++23 -Wall -Wextra strategy.cpp -o strategy-gcc && ./strategy-gcc
  SMS to Asha: order 1001 has shipped
  push to Ravi's phone: order 1002 has shipped
  e-mail from orders@example.com to Meera: order 1003 has shipped
the test strategy recorded 1 message: "order 2001 has shipped"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 strategy_session.py

Step through main() and watch the strategy the Notifier holds for each customer:

Interactive · step by step One Notifier, three strategies and a test Step through main() of strategy.cpp. Watch which strategy the Notifier holds for each customer; its own code never changes.
One Notifier, three strategies and a test, step 6 of 6. Showing The channels map, notifier (deliver_: email), under_test (records into sent), The customer (Kabir), Delivered (e-mail to Meera: order 1003), sent (1 message, for Kabir). In focus: sent.set_deliverydeliver_(message)push_backThe channels mapsms, email, pushnotifierdeliver_: emailunder_testrecords into sentThe customerKabirDeliverede-mail to Meera: order 1003sent1 message, for KabirKabir's order

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Three strategies, one type. The map holds a plain function, by_sms, a function object, ByEmail, with a sender of its own, and a lambda. All three are stored as Deliver, a std::function<void(const Message&)>.
  2. Asha prefers SMS. set_delivery(channels.at("sms")) gives the Notifier the plain function. order_shipped builds the message and calls deliver_: "SMS to Asha: order 1001 has shipped".
  3. Ravi prefers push. The next set_delivery swaps in the lambda, and the same order_shipped prints "push to Ravi's phone: order 1002 has shipped". Notifier's code did not change.
  4. Meera prefers e-mail. ByEmail carries its sender in a member, so the strategy has state of its own: "e-mail from orders@example.com to Meera: order 1003 has shipped".
  5. A strategy for tests. under_test gets a lambda that captures sent by reference and records each message instead of sending it. No mock class is needed.
  6. What the test sees. Kabir's order adds one message to sent instead of sending it, and the last line of the run says that the test strategy recorded 1 message.

The classic version would need an abstract DeliveryStrategy class and three subclasses for the same result. The last lines show the other gain: a test passes a lambda that records the messages instead of sending them, and needs no mock class. When the strategy is fixed while compiling, a template parameter, template <class Deliver> class Notifier, lets the compiler inline the call instead of going through std::function. Rule F.50 of the C++ Core Guidelines recommends a lambda wherever a function would need to capture local state, as the test strategy does with sent.

Checkpoint

Check: A strategy as a callable

  1. Question 1 of 2 How did strategy.cpp test Notifier without sending any message?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It passed a lambda that records each message in a vector

    A strategy is any callable with the right signature, so a test can pass a lambda that records the messages instead of sending them, and needs no mock class.

  2. Question 2 of 2 Every Notifier in a program delivers by SMS, and that is decided while compiling. Which form lets the compiler inline the delivery?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A template parameter, template <class Deliver> class Notifier

    std::function is the form for a strategy chosen at run time. When it is fixed while compiling, a template parameter lets the compiler inline the call instead of going through std::function.

A class hierarchy is still right for wide or open interfaces

Lambdas and variants are not always simpler. An interface: In C++, a class with only pure virtual functions and a public virtual destructor, and no data: it says what a type can do and leaves how to the classes that implement it. C++ has no interface keyword; this is a convention. More in “Abstract classes and interfaces in C++” with several related operations and some shared rules, such as a channel that sends, prices and retries, reads better as an abstract class with virtual functions. The Core Guidelines recommend a pure abstract class for an interface (rule C.121), and class hierarchies for concepts that really form a hierarchy (rule C.120). Code that other teams compile separately, such as plugins, needs that stable interface too. And a family of types that other code must be able to extend cannot be a std::variant, which lists every type it holds. Choose the smallest form that keeps the design honest; two questions about your own interface decide most of it:

Interactive · answer and compare A callable, a class hierarchy or a variant? Answer two questions about your own interface. The table marks the forms that fit and says why the others do not.
Does the interface have several related operations, such as send, price and retry, with rules they share?
Must code you do not compile, such as plugins, be able to add new types?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three modern forms of a pattern against the two things that decide between them
Form Several related operations Types added by other code
A callable (std::function) Holds: one operation behind one call signature Poor one std::function holds one operation, such as deliver in strategy.cpp Good any function, function object or lambda with the signature
An abstract class Holds: an interface of several virtual functions Good one interface for a channel that sends, prices and retries (C.121) Good a new class derives from it, even in a plugin compiled elsewhere
std::variant and std::visit Holds: one of a list of types named in advance Mixed each operation is a visitor over every type of the family Poor it lists every type it holds, so only its owner can add one
How the answers decide
  • “Does the interface have several related operations, such as send, price and retry, with rules they share?” Yes, several operations: adds a caveat to A callable (std::function). One callable holds one operation. Several related operations with shared rules read better as one pure abstract class (rule C.121).
  • “Must code you do not compile, such as plugins, be able to add new types?” Yes, other code adds types: rules out std::variant and std::visit. A variant names every type it can hold, so a family that other code must extend cannot be a variant.
  • “Does the interface have several related operations, such as send, price and retry, with rules they share?” No, one operation and “Must code you do not compile, such as plugins, be able to add new types?” No, my code has every type: adds a caveat to An abstract class. A class per algorithm for a single operation is the class-heavy form: a callable gives the same result with no classes.

This lesson covers the C++ side: lifetimes, value types and what the compiler checks. The Low-Level Design track uses these patterns in complete case studies with class designs, and the design-patterns track covers the whole catalogue, pattern by pattern, in several languages.

Checkpoint

Check: When a class hierarchy is still right

  1. Question 1 of 2 A channel must send, price and retry, and the three share rules. Which form suits it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A pure abstract class with three virtual functions

    An interface with several related operations and some shared rules reads better as an abstract class with virtual functions; the Core Guidelines recommend a pure abstract class for an interface (C.121).

  2. Question 2 of 2 Plugins built by other teams add new kinds of message. Can the family of messages be a std::variant with std::visit?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No, because a variant lists every type it holds, and only its owner can add one

    A std::variant suits a closed family that one piece of code owns. A family that other code must be able to extend cannot be a variant, which names every type it can hold.

Practise: patterns in their modern form

Nine questions on the modern forms, on strategy and on choosing a class hierarchy, one at a time, starting with the idea your answers have found hardest so far.

Practice that adapts to you

Practise this lesson

9 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 Every change to a document is stored as an object that can apply itself and reverse itself, so that the editor can undo it later. Which pattern is that?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: Command

    The command pattern turns an action into an object that can be stored, queued and reversed, which is what undo and redo need.

  2. Question 2 of 9 A code base has an abstract Visitor with accept() on every message class. What is the modern form for a closed set of message types?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: A std::variant of the message types, with std::visit

    Visitor over a closed set of types becomes a std::variant with std::visit. Strategy becomes a callable, observer holds weak_ptr, and a factory is a map from names to functions.

  3. Question 3 of 9 A new message type joins a closed family that every operation handles with std::visit. How do you find all the code that must change?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Add the type to the variant and build: every visitor without a handler for it fails to compile

    With std::visit the compiler reports a type that has no handler. Adding an alternative therefore turns into a list of compile errors, one per visitor to update.

  4. Question 4 of 9 In modern C++, what is usually the simplest form of the strategy pattern?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: A callable passed in, such as a lambda in a std::function or a template parameter

    A strategy is one replaceable piece of behaviour, and a callable is exactly that. std::function chooses it at run time; a template parameter chooses it while compiling. A class hierarchy still works, but it needs a class per algorithm for the same result.

  5. Question 5 of 9 channels is a const std::map<std::string, Deliver, std::less<>>, as in strategy.cpp. What does this line do?

    Read the code, then choose one answer.

    notifier.set_delivery(channels.at("fax"));
    Show the answer: Question 5 of 9

    Answer: It throws std::out_of_range, because there is no "fax" entry

    at() checks that the key exists and throws std::out_of_range when it does not, and it works on a const map. The operator [] would not compile on a const map, and on a non-const one it would insert an empty std::function.

  6. Question 6 of 9 Rule F.50 of the C++ Core Guidelines recommends a lambda when a function would need to capture local state. Which strategy of strategy.cpp needed that?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: The test strategy, which records into the local vector sent

    The test strategy writes to sent, a local of main, so it must capture it: exactly the case F.50 names. by_sms needs no state, and ByEmail keeps its own in a member.

  7. Question 7 of 9 What does rule C.121 of the C++ Core Guidelines recommend for a base class used as an interface?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Make it a pure abstract class

    Rule C.121 says that a base class used as an interface should be a pure abstract class, and C.120 keeps class hierarchies for concepts that really form a hierarchy.

  8. Question 8 of 9 When is a class hierarchy still a better fit than std::variant with std::visit?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: When other code, such as plugins, must add new types without changing yours

    A variant lists every type it can hold, so only its owner can add one. A class hierarchy is open: anyone can derive a new class. The other three choices are reasons to prefer the variant.

  9. Question 9 of 9 A sorting routine needs one comparison, chosen at each call site and known while compiling. Which form is the smallest that keeps the design honest?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: A template parameter that takes a callable

    Choose the smallest form that keeps the design honest: one operation known while compiling is a callable taken as a template parameter, as std::sort takes its comparison.

Observers that read freed memory, two ways, reproduced and fixed Pro: opens with a Pro, Premium or Ultimate pass

An observer stores references to objects it does not own, and that is where its bugs come from. This part reproduces the two classic failures under AddressSanitizer, a destroyed subscriber and a list that changes during a notification, then fixes both.

In this part:

  • A raw-pointer observer that calls a destroyed subscriber
  • A subscriber that subscribes during the notification
  • The fixed event: weak pointers, a snapshot and pruning

1 diagram · 4 runnable examples (C++, Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Factories that lose their entries, and commands sized for undo Pro: opens with a Pro, Premium or Ultimate pass

Two patterns store things for later: a factory keeps a table of creators, and a command history keeps every edit. This part reproduces how a self-registering factory loses entries when it moves into a static library, measures what an undo history costs in three designs, and ends with an exercise.

In this part:

  • A self-registering factory that loses its channels in a static library
  • Commands for undo and redo, sized three ways
  • Your turn: undo and redo for an editor

1 diagram · 6 runnable examples (C header, C++, C++ (main.cpp), C++ (push.cpp), C++ (registry.cpp), C++ (sms.cpp), Python) · about 6 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Visitors with std::variant: the compiler lists the case you forgot Pro: opens with a Pro, Premium or Ultimate pass

The visitor pattern exists so that one operation can handle every type of a family. With std::variant the family is closed and known to the compiler, which lets it refuse a visitor that misses a type. This part shows that check and compares it with classic double dispatch.

In this part:

  • Two visitors over three kinds of message
  • A new kind of message, and the error that names it
  • Double dispatch or std::variant

1 diagram · 3 runnable examples (C++, Python) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

A warm-up answered in full, then three design questions about events, plugins and undo:

  1. Fresher to mid · Warm-up

    Which classic design patterns get simpler in modern C++, and with which language or library features?

    Several of them lose most of their classes. Strategy becomes a callable: a lambda stored in a std::function when it is chosen at run time, or a template parameter when it is fixed while compiling. Observer keeps its subscribers through std::weak_ptr, so a destroyed subscriber is skipped instead of called. Factory becomes a map from names to functions that return std::unique_ptr, which also makes ownership of the new object explicit. Command becomes a plain struct, or a std::variant of structs, that holds enough data to apply and reverse an action. Visitor over a closed set of types becomes std::visit on a std::variant, and the compiler rejects a visitor that misses a type. A class hierarchy is still the right form for an interface with several related operations, or for types that other code must be able to add.

  2. Mid to senior

    Design an event bus for a desktop app whose subscribers are windows that can close at any moment. How do you avoid calling into a closed window, and what happens if a callback subscribes or unsubscribes while an event is being delivered?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    Plugins register themselves in a factory through static objects in their own .cpp files. After the plugins move into a static library, some are missing at run time, with no error at build time. Explain why, and give fixes with their trade-offs.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Implement undo and redo for a text editor that must keep the last 10,000 edits. How do you represent a command, how much memory does the history take, and how do you keep it bounded?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The classic patterns are still useful designs; modern C++ replaces most of their classes with callables, smart pointers and std::variant.
  • A strategy is a callable: std::function when it is chosen at run time, a template parameter when it is fixed while compiling, and a lambda that records makes a test strategy with no mock class.
  • An observer that holds std::weak_ptr skips subscribers that no longer exist, and a visitor over a closed family is a std::variant whose std::visit reports a type with no handler.
  • Prefer an abstract class when an interface has several related operations or must be extended by other code.

Terms in this lesson

design pattern
A named, reusable design for a problem that keeps coming back in object-oriented code, such as strategy, observer, factory, command or visitor, so that a whole design can be described in a word. In C++ the same idea often needs fewer classes than its classic form.
observer pattern
A design in which an event keeps a list of subscribers and tells each of them when it happens. In modern C++ the list holds std::weak_ptr, so a subscriber that has been destroyed is skipped instead of called.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.