Advanced 4 min · March 06, 2026

C++ Design Patterns — Static Init Fiasco & Pitfalls

Cross-Singleton dependencies crash apps even with Meyers Singleton.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • 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 build() method. If validation fails, they can return an 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 Comparison
Pattern CategoryCore GoalPopular ExampleWhen to Use in C++
CreationalAbstracting the instantiation processSingleton, Factory, BuilderWhen object creation logic is complex or may vary
StructuralOrganizing classes and objects for larger structuresAdapter, Decorator, FacadeWhen you need to compose interfaces without inheriting from concrete classes
BehavioralManaging communication between objectsObserver, Strategy, StateWhen objects need to interact without tight coupling
C++ IdiomaticLeveraging language-specific strengthsRAII, CRTP, PIMPLWhen 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 a std::unique_ptr to 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 declare virtual ~Base() = default; in the base class. If using an interface class, provide a virtual destructor with an empty body.
  • 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: Use std::weak_ptr in the subject's list of observers. Before calling update(), lock the weak pointer; if it's expired, remove it from the list.
  • 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
    The Open-Closed Principle states that software entities should be open for extension but closed for modification. The Strategy pattern achieves this by defining a family of algorithms in separate classes that implement a common interface. The context class holds a reference to the strategy interface and delegates behaviour to it. To add a new strategy, you write a new class that implements the interface — no modification to the context or existing strategies is needed. In C++, this is often implemented with std::function or a pure virtual interface Strategy.
  • 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
    In C++11 and later, the Meyers Singleton is thread-safe by the standard: static T& instance() { static T inst; return inst; }. The compiler ensures that the local static variable is initialised exactly once, even in multi-threaded code. Double-checked locking was used prior to C++11 to avoid locking on every access after the Singleton was already initialised. It involved: 1) check if instance is null without lock; 2) if null, lock; 3) check again; 4) create instance. It's error-prone due to memory ordering issues. Since C++11, the language guarantees safe initialisation, so double-checked locking is unnecessary (and still risky if implemented manually).
  • QCompare Runtime Polymorphism (Virtual Functions) and Static Polymorphism (CRTP). What are the memory and performance implications of each?SeniorReveal
    Runtime polymorphism uses vtables: each polymorphic class has a vtable pointer (typically 8 bytes per object on 64-bit). Each virtual call requires an indirect jump through the vtable, which can prevent inlining and cause branch misprediction. CRTP achieves compile-time polymorphism: no vtable, no indirect jumps, the function is resolved at compile time and can be inlined. However, CRTP locks the type at compile time — you cannot have a container of different CRTP-derived types (unless you use type erasure). Memory overhead: vtable pointer per object vs. zero overhead per object with CRTP (only the base template is empty). Performance: in latency-critical paths (HFT, game physics), CRTP can be 5–20% faster due to inlining and no vtable.
  • QDescribe the 'Object Pool' pattern. In what scenarios (e.g., Game Development) would you use this instead of standard new/delete allocation?Mid-levelReveal
    Object Pool is a creational pattern that pre-allocates a fixed number of objects and reuses them instead of allocating and deallocating individually. In game development, objects like bullets, particles, or enemies are created and destroyed frequently. Using new/delete leads to memory fragmentation and allocation latency spikes. An object pool maintains a fixed-size array or a free list. When an object is needed, it is taken from the pool (fast, O(1)), and when it's no longer needed, it's returned to the pool (not freed). This eliminates memory fragmentation and allocation overhead. In C++, the pool can be implemented using a union with an index to the next free node stored inside each pooled object, or using a std::vector of std::optional. It is also useful in embedded systems with limited heap.
  • 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
    The Visitor pattern lets you define a new operation on a set of objects without changing their classes. It works by having a 'visitor' interface with a visit method for each concrete class in the hierarchy. Each class accepts a visitor by calling visitor.visit(this). To add a new operation, you create a new class implementing the visitor interface — no changes needed to the existing classes. The main drawback is that adding a new concrete class to the hierarchy requires updating every existing visitor with a new visit() method (violating the Open-Closed principle). In C++, Visitor is often implemented with std::variant and std::visit as an alternative (pattern matching).

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

Previous
Memory Leaks and Debugging in C++
7 / 18 · C++ Advanced
Next
C++17 Features