C++ Design Patterns — Static Init Fiasco & Pitfalls
Cross-Singleton dependencies crash apps even with Meyers Singleton.
- Design patterns are reusable solutions to common software design problems
- Creational patterns abstract object creation (Factory, Singleton, Builder)
- Structural patterns compose classes (Adapter, Decorator, Facade)
- Behavioral patterns define how objects interact (Observer, Strategy, State)
- C++ adds unique idioms: RAII, CRTP, PIMPL
- Performance tradeoff: runtime polymorphism costs a vtable lookup; static polymorphism via CRTP eliminates it but requires compile-time strategy selection
Every large C++ codebase — game engines, trading systems, operating systems — is held together not just by algorithms but by structure. That structure comes from design patterns. When a new engineer joins the Unreal Engine team, they don't need to reverse-engineer why the rendering subsystem is built the way it is. The patterns make the intent obvious. That's the superpower patterns give you: code that communicates its own design decisions.
The problem patterns solve is accidental complexity — the kind that grows when you solve the same structural problem a dozen different ways across the same codebase. Without patterns, you end up with object creation scattered everywhere, tight coupling between subsystems that should never talk directly, and notification chains that are impossible to trace. Patterns give you a vocabulary and a discipline: a shared contract between you and every developer who reads your code in the future.
By the end of this article, you'll be able to implement the Gang of Four's most important patterns in modern C++ (C++17/20), understand the performance tradeoffs each one carries, recognise when a pattern is being abused, and walk into a senior engineering interview and explain not just how a pattern works but why it exists and when you'd reject it.
Creational Patterns: The Factory Method
In large-scale C++ applications, hardcoding object creation using the new keyword creates tight coupling. If you decide to change the underlying class implementation, you have to hunt down every instance of new across your project. The Factory Method pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created.
In modern C++, we often pair this with std::unique_ptr to ensure exception safety and automatic memory management, adhering to RAII principles.
But here's the thing: a factory switch-based on an enum is a code smell in itself. It violates the Open/Closed principle because every time you add a new product, you must modify the factory switch. Better alternatives include a registry of factory functions keyed by string (or type index) that can be extended without modifying the factory class. In HFT systems, that table is often populated at startup via static initialisation of plugin modules.
Structural Patterns: The Strategy Pattern & Modern CRTP
While traditional Strategy patterns rely on virtual function calls (runtime polymorphism), C++ allows for 'Static Polymorphism' via the Curiously Recurring Template Pattern (CRTP). This avoids the overhead of a VTable lookup, making it a favorite in high-frequency trading (HFT) and game physics engines.
By passing the derived class as a template parameter to the base class, the compiler can resolve function calls at compile-time, allowing for aggressive inlining.
But static polymorphism locks your strategy at compile time. If you need to swap strategies based on a runtime config file or user input, CRTP won't work — you'll need the classic Strategy pattern with virtual functions. The decision tree is simple: if the strategy is known when you write the code (e.g., different validators for different input types that never change), use CRTP. If the strategy is chosen during runtime (e.g., a compression algorithm selected from a UI dropdown), use virtual dispatch. Mix both in a hybrid: use a small virtual interface over a CRTP implementation to get inlining where possible and runtime selection where needed.
Creational Patterns: Thread-Safe Singleton (Meyers Singleton)
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. In C++, the most robust implementation is the 'Meyers Singleton' (named after Scott Meyers), which uses a function-local static variable. Since C++11, the initialization of function-local static variables is guaranteed to be thread-safe by the standard — no mutexes or double-checked locking needed.
But Singleton is often overused. It introduces global state, making code tightly coupled and hard to test. Before applying it, ask: does this truly need to be a single instance? Often a dependency injection framework or a factory that creates one instance (and passes it around) is cleaner. For truly unique resources (hardware driver, logging system, global configuration), Singleton is acceptable, but always pair it with a unit-testing seam: allow replacing the instance via a setter in testing builds.
Be aware of the static initialization order fiasco when two Singletons depend on each other (see production incident above). Meyers Singleton is lazy and thread-safe, but if one Singleton's constructor calls another Singleton, you can still get into trouble. Break cyclical dependencies by using explicit initialization steps in main().
Behavioral Patterns: The Observer Pattern (with Weak Pointers)
The Observer pattern defines a one-to-many dependency between objects: when one object changes state, all its dependents are notified automatically. In C++, implementing a thread-safe Observer requires careful resource management.
The classic pitfall: storing raw pointers to observers and not notifying them after they're destroyed — causes dangling pointer crashes. The fix: store std::weak_ptr<Observer> and lock before calling. Also protect the subscription list with a std::mutex because notification can happen from any thread.
Another consideration: the order of notification is undefined — never rely on it. And the notifier should not hold locks while calling observers (to avoid deadlocks). A common pattern is to copy the observer list under a lock, then iterate the copy outside the lock.
Creational Patterns: The Builder (Fluent Interface)
When a class requires many optional or interdependent parameters, constructor overloading becomes unmanageable. The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
In C++, a fluent Builder returns *this from setter methods, enabling method chaining. This is especially useful for configuration objects, database connections, or HTTP request builders. C++20 introduced designated initializers which can partially replace Builder for simple cases, but for validation and interdependent parameters, Builder is still the right tool.
One production gotcha: Builders often perform validation in their method. If validation fails, they can return an build()std::optional<T> or throw. Never leave the object in a partially constructed state. Another trap: Builders that own raw pointers instead of smart pointers — always store the final object in a std::unique_ptr or return by value if the object is movable.
| Pattern Category | Core Goal | Popular Example | When to Use in C++ |
|---|---|---|---|
| Creational | Abstracting the instantiation process | Singleton, Factory, Builder | When object creation logic is complex or may vary |
| Structural | Organizing classes and objects for larger structures | Adapter, Decorator, Facade | When you need to compose interfaces without inheriting from concrete classes |
| Behavioral | Managing communication between objects | Observer, Strategy, State | When objects need to interact without tight coupling |
| C++ Idiomatic | Leveraging language-specific strengths | RAII, CRTP, PIMPL | When you want to avoid runtime overhead or solve cross-cutting concerns |
Key Takeaways
- Design patterns are templates for solutions, not mandatory boilerplate. Use them only when the structural problem justifies the abstraction.
- Modern C++ (17/20) favors value semantics and smart pointers over raw pointers when implementing classic GoF patterns.
- Static polymorphism (CRTP) is a unique C++ tool that provides the benefits of the Strategy pattern without the runtime cost of virtual dispatch.
- Patterns facilitate a 'Ubiquitous Language' among developers, making high-level design reviews significantly more efficient.
- The Meyers Singleton is the safest Singleton in modern C++ — use it, but only when you genuinely need a single instance.
- Observer must use weak_ptr to avoid dangling listeners; always protect the observer list with a mutex if notifications can come from multiple threads.
Common Mistakes to Avoid
- Over-engineering with Abstract Factory where a simple function suffices
Symptom: Codebase has a class hierarchy for creating objects that have only two variants, maintained across multiple files with virtual methods, making change harder than direct instantiation.
Fix: Replace with a single factory function (or a lambda) that returns astd::unique_ptrto the product. Expand to a class only when you need multiple families or extension points. - Using Singleton as a global variable repository for the entire application
Symptom: Unit tests fail because Singletons hold state from previous tests; modules are tightly coupled to the Singleton, making them impossible to test in isolation.
Fix: Refactor to dependency injection: pass dependencies as constructor parameters. Use a factory that creates one instance but allows replacement during testing via a setter or an interface. - Forgetting virtual destructor in base class of a pattern hierarchy
Symptom: Memory leaks or undefined behavior when deleting derived object via base pointer; tools like Valgrind report 'definitely lost' memory.
Fix: Always declarevirtual ~in the base class. If using an interface class, provide a virtual destructor with an empty body.Base()= default; - Observer pattern with raw pointers and no cleanup on observer destruction
Symptom: Random crashes when subject notifies after some observers have been destroyed; debugger shows access to freed memory.
Fix: Usestd::weak_ptrin the subject's list of observers. Before calling, lock the weak pointer; if it's expired, remove it from the list.update() - Trying to use CRTP for runtime strategy selection
Symptom: Compilation error or template instantiation explosion when trying to pass a CRTP type selected at runtime.
Fix: CRTP is compile-time polymorphism. For runtime strategy selection, use virtual functions or a combination: a small virtual interface that wraps CRTP implementations.
Interview Questions on This Topic
- QExplain the 'Open-Closed Principle' and show how the Strategy pattern helps a class adhere to it.SeniorReveal
- QHow would you implement a thread-safe Singleton in C++11? What is 'Double-Checked Locking' and why is it usually unnecessary in modern C++?SeniorReveal
- QCompare Runtime Polymorphism (Virtual Functions) and Static Polymorphism (CRTP). What are the memory and performance implications of each?SeniorReveal
- QDescribe the 'Object Pool' pattern. In what scenarios (e.g., Game Development) would you use this instead of standard
new/deleteallocation?Mid-levelReveal - QWhat is the 'Visitor' pattern, and how does it allow you to add new operations to existing class hierarchies without modifying them? What is its main drawback?SeniorReveal
Frequently Asked Questions
Is the Singleton pattern an anti-pattern in C++?
It is often called an anti-pattern because it introduces global state, making code brittle and hard to test. However, for genuinely unique resources (like a hardware driver interface or a unique log file), it is valid. In C++11 and later, the 'Meyers Singleton' (using a static local variable) is the thread-safe standard implementation.
What is the PIMPL idiom and why is it a structural pattern?
PIMPL (Pointer to IMPLementation) is a structural pattern used to hide a class's private members in a separate implementation class. This reduces compilation times (by breaking header dependencies) and provides a stable ABI (Application Binary Interface).
Should I use std::shared_ptr or std::unique_ptr for patterns?
Prefer std::unique_ptr by default for patterns like Factory or Builder. It represents clear ownership. Only move to std::shared_ptr if you have multiple objects that truly need to share ownership of the same resource, such as in a complex Observer notification graph.
How do I implement a thread-safe Observer in C++?
A thread-safe Observer requires a std::mutex to protect the list of subscribers. Furthermore, you should use std::weak_ptr to store observers. This prevents 'lapsed listener' memory leaks—before notifying, you attempt to lock the weak pointer; if it's expired, the observer has been destroyed and you can safely remove it from your list.
What is the difference between the Strategy pattern and the State pattern?
Both use composition to change behaviour. The key difference: in Strategy, the client sets the algorithm, and it stays that way until changed. In State, the object changes its behaviour automatically based on internal state transitions — the state context knows the allowed transitions. Strategy is about choosing an algorithm; State is about managing state-dependent behaviour.
That's C++ Advanced. Mark it forged?
4 min read · try the examples if you haven't