2Brute2Best
πŸ”₯0
M8.1

Design Patterns Quick-Strike for C++ Interviews

Medium High-frequency interview topic Pro OnlyThread-Safe SingletonFactory & StrategyObserver with weak_ptrDecorator & AdapterRAII & Pimpl Idiom
⚑ 15-Second Mental ModelM8.1 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Core Intuition)

In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.

Rule to Remember: Make illegal states impossible to compile or run.
3 Golden Facts to Remember
Fact #1
Why is Meyers' Singleton considered thread-safe in C++11, and how does the compiler implement it?

Section 6.7 of the C++11 standard mandates that if control enters the declaration of a local static variable concurrently while the variable is being initialized, the concurrent execution shall wait until the initialization completes. Compilers like GCC, Clang, and MSVC implement this using an internal 8-byte guard variable in .bss and atomic operations (__cxa_guard_acquire and __cxa_guard_release). The first thread acquires the guard, runs the constructor, and sets the guard bit; other threads wait on a futex/event. Subsequent reads check the guard with a single acquire branch without mutex overhead.

Fact #2
What is the complete Rule of 5 requirement when implementing the Pimpl idiom with std::unique_ptr?

Because std::unique_ptr is move-only, the enclosing class becomes move-only by default. You must:

  1. Declare ~Widget(); in the header, and define Widget::~Widget() = default; in .cpp after Impl is complete.
  2. Declare move constructor Widget(Widget&&) noexcept; and move assignment Widget& operator=(Widget&&) noexcept; in the header, defining both as = default in .cpp.
  3. If copy semantics are desired, implement deep copying manually in .cpp (pimpl_ = std::make_unique<Impl>(*other.pimpl_)), as compiler-generated copy is deleted by std::unique_ptr.
Fact #3
Why is std::weak_ptr preferred over std::shared_ptr or raw pointers for Observer patterns?

Raw pointers (IObserver*) become dangling pointers if an observer is deleted, causing segmentation faults on notify(). Storing std::shared_ptr<IObserver> keeps the observer alive forever even if the rest of the application discarded its reference, causing silent memory leaks and zombie notifications. std::weak_ptr<IObserver> breaks ownership: it tracks the control block without incrementing strong_count. wp.lock() safely returns null if the observer has expired, allowing automatic pruning during notification.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

Interviewers don't test design patterns in C++ to hear Java textbook definitions; they want to see if you understand C++ memory ownership, compile-time vs runtime costs, and lifetime safety. Weak candidates write thread-unsafe singletons with raw pointers, create circular memory leaks with std::shared_ptr observers, and trigger compiler errors with incomplete types in Pimpl destructors. Strong candidates immediately deploy thread-safe Meyers' Singletons, leak-free std::weak_ptr observers with auto-pruning, zero-overhead C++20 concept policies, and explain the exact ABI and compilation firewall mechanics of the Pimpl idiom.

02

Under the hood & memory layout

Hardware & memory representation

Every C++ design pattern manifests as a distinct physical machine layout: Meyers' Singleton relies on a compiler-generated 8-byte guard variable (__cxa_guard_acquire / __cxa_guard_release) in the .bss section, guaranteeing single-execution thread safety with zero mutex overhead after initial initialization. Runtime polymorphism (Factory, Strategy, Decorator) incurs an 8-byte vptr in each object pointing to a vtable in .rodata, requiring two pointer dereferences per call. In contrast, compile-time policy strategies instantiate template code directly, allowing -O2 to inline behavior directly into the caller frame with zero runtime bytes. Observer with std::weak_ptr avoids circular retention by inspecting the 16-byte control block's strong_count without incrementing it. Pimpl shrinks the visible class to a single 8-byte std::unique_ptr<Impl>, freezing class size across ABI boundaries.

Meyers' Singleton Guard Variable

Under the Itanium ABI, a local static object possesses an associated 64-bit guard variable in .bss. The first calling thread executes an atomic test-and-set acquire barrier; subsequent calls bypass initialization with a single branch instruction.

Runtime Strategy vs Policy Layout

A runtime strategy requires an 8-byte heap pointer + 8-byte object vptr + non-inlinable dynamic dispatch. A C++20 concept policy generates monomorphic code where the compiler can inline the algorithm directly with 0 vtable overhead.

std::weak_ptr Observer Control Block

Storing std::weak_ptr<IObserver> occupies 16 bytes (8B raw pointer + 8B control block pointer). It increments weak_count but leaves strong_count untouched. When strong_count drops to 0, the observer's destructor runs immediately.

Pimpl Idiom Compilation Firewall

The public class contains only std::unique_ptr<Impl> (8 bytes). Adding or modifying private members inside struct Impl in the .cpp file preserves sizeof(Host) and member offsets, eliminating header recompilation across dependent translation units.

03

Core concepts & syntax traps

01

Thread-Safe Meyers' Singleton & The Dependency Injection Trade-Off

In C++11 and beyond (Β§6.7 [stmt.dcl]), static local variables with initializers are guaranteed to be initialized thread-safely upon first execution. This is known as Meyers' Singleton. The compiler emits atomic guard instructions so multiple threads racing to call getInstance() synchronize safely without explicit locks. To guarantee uniqueness, you must delete both the copy constructor and copy assignment operator, as well as the move operations. However, in production architectures, singletons introduce hidden global state and hinder unit testing; modern systems prefer Dependency Injection (DI) where dependencies are passed explicitly as std::shared_ptr or references.

C++23 snippet
class DatabaseConfig {
public:
    static DatabaseConfig& instance() {
        static DatabaseConfig inst; // Thread-safe since C++11
        return inst;
    }
    DatabaseConfig(const DatabaseConfig&) = delete;
    DatabaseConfig& operator=(const DatabaseConfig&) = delete;
private:
    DatabaseConfig() { /* heavy init */ }
};
Interview takeaway:

Never write raw pointer double-checked locking; use Meyers' Singleton or inject instances via Dependency Injection.

02

Factory Method & Self-Registering Plugin Registry

A classic Factory Method creates objects through an interface without coupling callers to concrete classes. In modern C++, factories return std::unique_ptr<Interface>, establishing unambiguous move-only heap ownership. To make a factory extensible without modifying its switch statements, use a Self-Registering Registry: concrete classes register a factory lambda into an internal std::unordered_map<std::string, Creator> using a static helper object before main() executes.

C++23 snippet
class WidgetFactory {
public:
    using Creator = std::function<std::unique_ptr<IWidget>()>;
    static WidgetFactory& instance() { static WidgetFactory f; return f; }
    void registerWidget(std::string_view name, Creator c) { map_[std::string(name)] = std::move(c); }
    std::unique_ptr<IWidget> create(std::string_view name) const {
        auto it = map_.find(std::string(name));
        return (it != map_.end()) ? it->second() : nullptr;
    }
private:
    std::unordered_map<std::string, Creator> map_;
};
Interview takeaway:

Return std::unique_ptr<T> from factories; use a self-registering map to eliminate OCP-violating switch statements.

03

Strategy Pattern: Runtime Polymorphism vs Compile-Time C++20 Concepts

The Strategy pattern decouples an algorithm from its enclosing context. C++ offers two architectural options:

  1. Runtime Strategy (std::unique_ptr<IStrategy>): Flexible dynamic swapping at runtime at the cost of heap allocation, pointer indirection, and non-inlinable virtual table calls.
  2. Compile-Time Strategy (Policy-Based Design with C++20 Concepts): The algorithm is parameterized as a template argument constrained by a concept. The compiler resolves dispatch at compile-time with zero heap allocation, zero vptr overhead, and aggressive function inlining.
C++23 snippet
// Runtime Strategy:
struct ISort { virtual ~ISort() = default; virtual void sort(std::span<int>) = 0; };

// Compile-Time Policy (C++20 Concept):
template <typename T>
concept SortPolicy = requires(T s, std::span<int> data) { { s.sort(data) } -> std::same_as<void>; };

template <SortPolicy Policy>
class Dataset { Policy policy_; public: void run(std::span<int> d) { policy_.sort(d); } };
Interview takeaway:

Use runtime strategy when behavior must swap dynamically based on config; use C++20 policy concepts for high-frequency low-latency code.

04

Observer Pattern with std::weak_ptr (Preventing Memory Leaks & Dangling References)

The classic Observer interview trap asks: "What happens if an observer is deleted while registered with the Subject?"

  • If the Subject stores raw pointers (IObserver*), calling notify() causes a catastrophic dangling pointer use-after-free crash.
  • If the Subject stores std::shared_ptr<IObserver>, the Subject keeps listeners alive indefinitely, creating memory leaks and circular references. The Senior C++ Solution: The Subject stores std::vector<std::weak_ptr<IObserver>>. Inside notify(), the Subject attempts promotion with wp.lock(). If wp.expired(), the observer was deleted elsewhere, and std::erase_if prunes it from the list automatically!
C++23 snippet
void notify(std::string_view msg) {
    std::erase_if(observers_, [&](const std::weak_ptr<IObserver>& wp) {
        if (auto sp = wp.lock()) {
            sp->onEvent(msg);
            return false; // Retain active observer
        }
        return true; // Auto-prune expired observer!
    });
}
Interview takeaway:

Always store std::weak_ptr in event broadcasters to prevent circular memory leaks and dangling callback crashes.

05

Decorator & Adapter Patterns in Modern C++

The Decorator pattern attaches additional responsibilities to an object dynamically by wrapping std::unique_ptr<IComponent> and forwarding calls while injecting cross-cutting behavior (logging, encryption, rate-limiting, compression). The Adapter pattern converts an incompatible existing class interface into a target interface expected by callers without altering the underlying class.

C++23 snippet
class EncryptionDecorator : public IDataStream {
    std::unique_ptr<IDataStream> wrapped_;
public:
    explicit EncryptionDecorator(std::unique_ptr<IDataStream> w) : wrapped_(std::move(w)) {}
    void write(std::string_view s) override {
        std::string enc = encrypt(s);
        wrapped_->write(enc);
    }
};
Interview takeaway:

Decorators wrap the same interface to augment behavior; Adapters bridge two different interfaces.

06

The C++ Pimpl Idiom & The unique_ptr Destructor Trap

The Pimpl (Pointer to Implementation) idiom hides private data members and heavy third-party #include headers behind a forward-declared struct Impl; and a std::unique_ptr<Impl> pimpl_;. Benefits: Fast compilation (client files do not recompile when private members change) and stable ABI. The Destructor Trap: If you write ~Widget() = default; in Widget.h, the compiler generates the destructor code at the header call site. At that point, struct Impl is incomplete! std::unique_ptr<Impl>::~unique_ptr requires a complete type to invoke sizeof(Impl) inside delete. This fails with a static assertion or compiler error. Rule: Always declare ~Widget(); in the header, and define Widget::~Widget() = default; in the .cpp file after struct Impl is fully defined!

C++23 snippet
// Widget.h
class Widget {
public:
    Widget();
    ~Widget(); // MUST NOT be = default in .h!
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
private:
    struct Impl;
    std::unique_ptr<Impl> pimpl_;
};

// Widget.cpp
struct Widget::Impl { std::string cache; /* heavy libs */ };
Widget::Widget() : pimpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // Legal here because Impl is complete!
Interview takeaway:

With Pimpl, always define destructors and move operators in the .cpp file where the Impl struct is complete.

Brute2Best Pro Content

Unlock Live Implementation & Interview Scenarios

The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering