Patterns and Idioms

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

RAII

Covered fully in Memory Management and Smart Pointers — the foundational C++ idiom nearly everything below builds on.

Pimpl (Pointer to Implementation)

Hides a class’s private implementation details behind an opaque pointer, so changing them doesn’t force every includer to recompile — a real compile-time/ABI-stability win, not just an aesthetic one:

// widget.h -- unchanged even if Impl's members change; includers never see Impl's definition
#include <memory>
class Widget {
public:
    Widget();
    ~Widget();                        // must be declared here, DEFINED in the .cpp (see below)
    void doSomething();
private:
    class Impl;                        // incomplete type -- fine as a unique_ptr target
    std::unique_ptr<Impl> impl_;
};
// widget.cpp
#include "widget.h"

class Widget::Impl {
public:
    void doSomething() { /* real implementation, changeable without recompiling includers */ }
};

Widget::Widget() : impl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;    // MUST be defined where Impl is complete -- unique_ptr's destructor needs
                                   // to know Impl's full definition, which the header alone doesn't provide
void Widget::doSomething() { impl_->doSomething(); }

NVI (Non-Virtual Interface)

The public interface is non-virtual and calls a private/protected virtual "hook" — the base class controls when and how the hook runs (validation, logging, timing) around it, something a plain public virtual function cannot enforce:

class Shape {
public:
    double area() const {              // public, non-virtual -- callers use only this
        validateState();
        return computeArea();            // the actual customization point
    }
    virtual ~Shape() = default;

private:
    virtual double computeArea() const = 0;   // private virtual -- derived classes override, but cannot
    void validateState() const { /* ... */ }    // be called directly, and cannot skip validateState()
};

CRTP and Static Polymorphism (with deducing this)

The Curiously Recurring Template Pattern gets polymorphism-like customization resolved at compile time, with no vtable/virtual-call overhead:

template <typename Derived>
class Comparable {
public:
    bool operator<(const Derived& other) const {
        return static_cast<const Derived&>(*this).compareTo(other) < 0;   // "virtual" call resolved statically
    }
};

class Version : public Comparable<Version> {
public:
    explicit Version(int v) : value_(v) {}
    int compareTo(const Version& other) const { return value_ - other.value_; }
private:
    int value_;
};

C++23’s deducing this (this Self&& self as an explicit first parameter) replaces most CRTP use cases with plain, more readable inheritance:

class ComparableV2 {
public:
    template <typename Self>
    bool operator<(this const Self& self, const Self& other) {   // "self" deduces to the most-derived type --
        return self.compareTo(other) < 0;                          // no CRTP base-template needed at all
    }
};

class VersionV2 : public ComparableV2 {
public:
    explicit VersionV2(int v) : value_(v) {}
    int compareTo(const VersionV2& other) const { return value_ - other.value_; }
private:
    int value_;
};

Mixins

Small, composable base classes that each add one capability, combined via multiple inheritance — distinct from CRTP in that a mixin doesn’t need to know the derived type at all:

class Loggable {
public:
    void log(const std::string& msg) const { std::cout << "[log] " << msg << '\n'; }
};
class Serializable {
public:
    std::string serialize() const { return "{}"; }
};

class Widget2 : public Loggable, public Serializable {   // gains both capabilities with no code duplication
};

Type Erasure

Hides a concrete type behind a uniform interface, without an explicit inheritance relationship — std::function (Functions and Lambdas) and std::any (Vocabulary Types) are standard-library examples; the pattern itself:

#include <memory>
#include <string>

class Drawable {
public:
    template <typename T>
    Drawable(T obj) : self_(std::make_unique<Model<T>>(std::move(obj))) {}   // T need not derive from anything
    std::string draw() const { return self_->draw(); }

private:
    struct Concept { virtual ~Concept() = default; virtual std::string draw() const = 0; };
    template <typename T>
    struct Model : Concept {
        explicit Model(T obj) : obj_(std::move(obj)) {}
        std::string draw() const override { return obj_.draw(); }
        T obj_;
    };
    std::unique_ptr<Concept> self_;
};
// any type with a "std::string draw() const" method can be stored in a Drawable, with no shared base class

The Named-Parameter Idiom

C++ has no native named arguments; a builder-style chain simulates them for constructors/functions with many optional parameters:

class HttpRequestBuilder {
public:
    HttpRequestBuilder& url(std::string u) { url_ = std::move(u); return *this; }
    HttpRequestBuilder& timeout(int seconds) { timeout_ = seconds; return *this; }
    HttpRequestBuilder& retries(int n) { retries_ = n; return *this; }
private:
    std::string url_;
    int timeout_ = 30, retries_ = 0;
};

// HttpRequestBuilder{}.url("https://example.com").timeout(5).retries(3);

Designated initializers (C++20, see Constants, Enumerations, and Initialization) cover the simpler case of an aggregate with named fields, with no builder needed.

Attorney-Client

Grants one specific class limited, explicit access to another’s private members, narrower than a blanket friend:

class Widget3 {
private:
    void internalReset() { /* ... */ }
    friend class WidgetAttorney;   // only WidgetAttorney (the "attorney"), not everyone, gets in
};

class WidgetAttorney {              // the ONLY class allowed to call internalReset() -- acts as a
public:                              // narrow, audited gatekeeper instead of a wide-open friend declaration
    static void resetFor(Widget3& w) { w.internalReset(); }
};

Factories Without if/else

A registry of creation functions, keyed by a discriminator, replaces a long if/else-if (or switch) chain and lets new types register themselves without editing the factory’s own code:

#include <functional>
#include <unordered_map>
#include <memory>
#include <string>

class Shape { public: virtual ~Shape() = default; };
class Circle : public Shape {};
class Square : public Shape {};

class ShapeFactory {
public:
    using Creator = std::function<std::unique_ptr<Shape>()>;

    void registerShape(const std::string& name, Creator creator) {
        creators_[name] = std::move(creator);
    }
    std::unique_ptr<Shape> create(const std::string& name) const {
        auto it = creators_.find(name);
        return it != creators_.end() ? it->second() : nullptr;
    }
private:
    std::unordered_map<std::string, Creator> creators_;
};

// factory.registerShape("circle", [] { return std::make_unique<Circle>(); });
// factory.registerShape("square", [] { return std::make_unique<Square>(); });
// auto shape = factory.create("circle");

Thread-Safe Singleton

Since C++11, a function-local static is guaranteed to be initialized exactly once, even under concurrent first-time access — no manual double-checked locking needed:

class Logger {
public:
    static Logger& instance() {
        static Logger singleton;    // thread-safe initialization guaranteed by the language itself (C++11+)
        return singleton;
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;
};