Skip to content

C++ Idioms

C++ Idioms

Last updated: 2026-05-17 · 16 min read

C++ idioms matter especially because the language permits raw pointers, manual memory management, exceptions, multiple inheritance, and template metaprogramming all at once. Community conventions that encode invariants the compiler does not enforce act as the de facto safety boundaries. The key point is that, unlike Rust's borrow checker, these are maintained by people rather than enforced by tooling.

Personal note: Ecosystems that prioritize low-level control, such as China's, tend to revolve around C++.

RAII (Resource Acquisition Is Initialization)

An idiom that ties resource acquisition to object lifetime: acquire in the constructor, release in the destructor. It is the foundation of C++ safety.1

Problem it solves: Resources are never leaked regardless of the exit path from a scope, whether via exception, early return, or break. It is essentially the compiler automating C's goto cleanup pattern.

C++
{
    std::lock_guard<std::mutex> lock(mtx);  // Acquisition
    // Whether an exception is thrown, a return is executed, or the function exits normally
    // the lock is automatically released when the scope exits
}

Representative examples: std::lock_guard, std::unique_ptr, std::fstream, std::scoped_lock.

Rule of Zero / Three / Five

A meta-idiom governing how to handle the special member functions (destructor, copy constructor, copy assignment, move constructor, move assignment) of a resource-owning class. Marshall Cline first formalized the Rule of Three in 1991, and it was extended to the Rule of Five when move operations were added in C++11.2

  • Rule of Zero (most recommended): Do not write special member functions directly; delegate to RAII components. The compiler-generated versions handle everything.

  • Rule of Three (C++98): If you define any one of the destructor, copy constructor, or copy assignment operator, you must define all three.

  • Rule of Five (C++11): Consider all five together: the three above plus the move constructor and move assignment operator.

Problem it solves: Double-free, leaks, and partial state when a resource-owning class is copied or moved.

C++
// Rule of Zero - recommended
class Buffer
{
    std::vector<int> data;   // The vector handles copy/move/destruction automatically
    std::string name;
    // No special member functions written
};

// Rule of Five - only when managing OS resources directly
class FileHandle
{
public:
    explicit FileHandle(const char* path);
    ~FileHandle();                                     // (1)
    FileHandle(const FileHandle&) = delete;            // (2) Prohibit Copying
    FileHandle& operator=(const FileHandle&) = delete; // (3)
    FileHandle(FileHandle&&) noexcept;                 // (4)
    FileHandle& operator=(FileHandle&&) noexcept;      // (5)
private:
    int fd;
};

Smart pointer ownership

Express ownership through types. Use raw pointers primarily as non-owning observers.3

  • std::unique_ptr<T>: exclusive ownership. Move-only; copying is not allowed. Virtually zero overhead.

  • std::shared_ptr<T>: shared ownership. The last owner's destruction triggers the release, tracked by a reference count in the control block. Copying and destroying a shared_ptr incurs synchronization overhead for reference count manipulation. Note that only the count is atomic; the pointed-to object itself is not made thread-safe.

  • std::weak_ptr<T>: a non-owning observer of a shared_ptr. It does not extend the lifetime (the strong count is not incremented). However, the control block is not released until the weak count also reaches zero, so it is not entirely free. Its primary use is breaking circular references.

Problem it solves: The ownership ambiguity of "who should delete this pointer." Looking at a raw pointer's function signature alone, you cannot tell whether it implies ownership or merely observation.

C++
auto conn = std::make_unique<Connection>(host, port);  // Exclusive ownership
auto cfg  = std::make_shared<Config>();                // Shared ownership
std::weak_ptr<Config> watcher = cfg;                   // Observation only, no strong count increment

void use(Connection* c);  // Non-owning parameter — the most common legitimate use of raw pointers in application code
use(conn.get());

Selection order: unique_ptr first; use shared_ptr only when shared semantics are genuinely required; use raw pointers or T& for non-owning references.

Using a raw pointer as a non-owning view is the rule for application code. There are boundaries where raw pointers are legitimately used for ownership or other purposes:

  • C API boundaries (malloc/free, OS handles)

  • arena/pool allocator internals

  • intrusive data structures

  • placement new

  • legacy ABI compatibility

  • allocation-free embedded/real-time environments

Personal note: In practice, raw pointer dogma tends to be stronger than the RAII and ownership model. The difficulty of C++ comes not from the syntax but from the people.

copy-and-swap

An idiom that implements the copy assignment operator by taking a copy by value and then swapping. Herb Sutter's Guru of the Week #59 (1999) is the most widely cited formulation.4

Problem it solves: Exception safety and self-assignment (a = a) handling in one stroke. Even if an exception occurs mid-assignment, the object is never left in a partial state. If constructing the new copy fails, the original remains intact.

C++
// If you use non-owning members, the Rule of Zero handles everything for you.
// We use `vector` here to keep the example simple; if you owned the data via a raw pointer,
// keep in mind that all five functions of the Rule of Five would be required.
class Resource
{
public:
    Resource& operator=(Resource other)   // ← by value
    {
        swap(*this, other);
        return *this;
    }

    friend void swap(Resource& a, Resource& b) noexcept
    {
        using std::swap;
        swap(a.data, b.data);
        swap(a.name, b.name);
    }
private:
    std::vector<int> data;
    std::string name;
};

Trade-off: copy-and-swap guarantees strong exception safety but always creates a temporary object, making it difficult to reuse existing storage. Since C++11, a single by-value assignment operator can partially unify copy assignment and move assignment, because when an rvalue is passed the move constructor builds other. That said, performance-sensitive container and buffer types are better served by implementing copy assignment and move assignment separately, with the move assignment performing only a swap.


2. Encapsulation and Interface Design

Pimpl (Pointer to Implementation)

The class body holds implementation details only as a pointer to a forward-declared type; the actual implementation is defined in the .cpp file. Also known as the "Cheshire Cat" or "compilation firewall." Herb Sutter's GotW #24 and its C++11 updates GotW #100/#101 are the most frequently cited references.5

Problem it solves: Blocking recompilation cascades caused by header changes, ensuring ABI stability, and preventing private dependency headers from being exposed to clients.

C++
// widget.h
class Widget
{
public:
    Widget();
    ~Widget();
    void draw();

    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
private:
    struct Impl;                     // forward declaration only
    std::unique_ptr<Impl> impl;
};

// widget.cpp
struct Widget::Impl
{
    Renderer renderer;               // actual members
    State state;
};

Widget::Widget() : impl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;         // defined at the point where `Impl` is fully defined

Cost: One dynamic allocation and one level of indirection per call. Not suitable for hot paths.

Caution 1: Do not place the destructor or move operations as = default in the header. If std::unique_ptr is instantiated while Impl is still an incomplete type, a compile error results. Define them in the .cpp file. This is a commonly missed trap, worth starring.

Caution 2: If std::unique_ptr<Impl> is a member, the compiler-generated copy constructor and copy assignment operator are automatically deleted (since unique_ptr is non-copyable). If the Pimpl object needs to be copyable, you must explicitly define the copy constructor and copy assignment operator to deep-copy Impl.

NVI (Non-Virtual Interface)

An idiom in which the public interface is non-virtual while actual polymorphism is implemented via private virtual functions. Herb Sutter explicitly recommended this in his 2001 C/C++ Users Journal article "Virtuality," after which it became a standard guideline.6

Problem it solves: Separation of interface from behavior. The base class can enforce cross-cutting concerns such as pre/post conditions, argument validation, and logging. Derived classes implement only the behavior.

C++
class Document
{
public:
    virtual ~Document() = default;    // polymorphic base requires a virtual destructor

    void save()                       // public non-virtual
    {
        validate();
        do_save();                    // polymorphism point
        log_saved();
    }
private:
    virtual void do_save() = 0;       // part to be implemented by derived classes
    void validate() const;
    void log_saved() const;
};

A polymorphic base class used to handle derived objects through base class pointers or references must always declare a virtual destructor explicitly. Without it, calling delete base_ptr will not invoke the derived class destructor.


3. Type-Level Representation

Representing absence with std::optional

When a value may or may not be present, use std::optional<T> instead of nullptr or sentinel values such as -1 or an empty string.7

Problem it solves: nullptr dereferences, ad-hoc conventions like "negative means failure," and function signatures that hide the possibility of failure.

C++
std::optional<User> find_user(UserId id)
{
    if (auto record = db.lookup(id)) {
        return User(*record);
    }
    return std::nullopt;
}

if (auto user = find_user(id)) {
    process(*user);                  // inside with *user or user->method()
}

Trade-off: If there are multiple failure reasons, std::expected<T, E> (C++23) or a Result-style type is more appropriate. optional only represents two states: present or absent.

Note: std::optional<T&> is not included in the standard even as of C++23 (it is at the C++26 proposal stage). For references, use std::optional<std::reference_wrapper<T>> or a raw pointer T*.

Representing sum types with std::variant + std::visit

A type-safe union that holds one of several types. std::visit enables dispatch that closely resembles pattern matching.8

Problem it solves: Representing a closed set of types without an inheritance hierarchy. Avoids dynamic_cast. Using an overload set allows the compiler to verify that every alternative is handled.

C++
using Shape = std::variant<Circle, Square, Triangle>;

double area(const Shape& s)
{
    return std::visit([](const auto& shape) {
        return shape.area();        // All types must have `area()`
    }, s);
}

// Type-specific handling via overload set
template<class... Ts>
struct Overloaded : Ts...
{
    using Ts::operator()...;
};

template<class... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;

double area2(const Shape& s)
{
    return std::visit(Overloaded {
        [](const Circle& c)   { return 3.14 * c.r * c.r; },
        [](const Square& q)   { return q.side * q.side; },
        [](const Triangle& t) { return 0.5 * t.base * t.height; }
    }, s);
}

A note on exhaustiveness: The two examples above differ in how much they enforce exhaustiveness.

  • The generic lambda in area only enforces that every alternative must support calling area(). It does not enforce explicit per-type handling.

  • The overload set in area2 requires a matching overload for each alternative in order to compile. This allows missing cases in a closed type set to be caught at compile time. However, mixing in a generic lambda (auto) or a catch-all overload weakens exhaustiveness.

Trade-off: Increased compile times. If the set of types is frequently extended, a virtual function hierarchy is more flexible.


4. Template and Compile-Time Idioms

CRTP (Curiously Recurring Template Pattern)

A pattern in which a derived class passes itself as a template argument to its base class. James Coplien named the pattern in 1995 in C++ Report after observing it recurring in early C++ template code.9 Used for static polymorphism, mixins, and policy injection.

Problem it solves: Interface reuse without the runtime cost of virtual functions (vtable lookup). Static dispatch. And the mixin effect of having the base class "inject" code into the derived class.

C++
template<typename Derived>
class Comparable
{
public:
    bool operator!=(const Derived& other) const
    {
        return !static_cast<const Derived&>(*this).operator==(other);
    }
    bool operator<=(const Derived& other) const
    {
        const auto& self = static_cast<const Derived&>(*this);
        return self < other || self == other;
    }
};

class Point : public Comparable<Point>
{
public:
    bool operator==(const Point& o) const { return x == o.x && y == o.y; }
    bool operator<(const Point& o)  const { return x < o.x || (x == o.x && y < o.y); }
private:
    int x, y;
};

Cost: The interface is fixed at compile time, so it is unsuitable for polymorphism that requires changing types at runtime. Code bloat is also a possibility.

Relationship with C++20 concepts: If CRTP was used to check interface conformance (expressing that a derived type provides certain operations), concepts replace it far more cleanly. However, the mixin use case where the base class injects methods into the derived class, as in the Comparable<Point> example above, is not replaced by concepts. That use case still requires CRTP or another composition technique.

Type erasure

An idiom that hides the concrete type and exposes only an interface, using virtual functions or a function pointer table internally while not leaking type information to the outside. Sean Parent's GoingNative 2013 talk "Inheritance Is The Base Class of Evil" is frequently cited as the inflection point that popularized value-semantics-based type erasure.10

Problem it solves: Representing polymorphic containers without templates, reducing header dependencies, and ensuring ABI stability.

C++
// Type erasure examples from the standard library
std::function<int(int)> f = [](int x) { return x * 2; };
f = some_free_function;             // Any callable object works as long as the signature matches

std::any value = 42;
value = std::string("hello");       // Any type can be stored

Representative examples: std::function, std::any, and the deleter of std::shared_ptr.

Cost: Possible heap allocation (partially mitigated by Small Object Optimization) and the cost of indirect calls.

EBO (Empty Base Optimization)

An idiom that exploits the compiler rule allowing an empty base class to add no extra padding to a derived class. More precisely, an empty base subobject can share the same address as another subobject in the derived class, eliminating any size waste.11

Problem it solves: Keeping a stateless type such as a policy class or allocator as a member guarantees at least 1 byte of overhead by the standard. Making it a base class eliminates that byte.

C++
struct EmptyDeleter {};

template<typename T, typename Deleter>
class UniquePtr : private Deleter   // EBO: if the deleter is an empty type, `sizeof` equals only `T*`
{
    T* ptr;
};

Since C++20, the same optimization is available by applying the [[no_unique_address]] attribute to a member, so the practice of using inheritance specifically for EBO is becoming less common.


5. STL Usage Idioms

Erase-remove idiom

A pattern that combines std::remove_if with erase to delete elements matching a condition from a container.12

Problem it solves: Despite its name, std::remove_if does not actually delete elements. It only shuffles candidates to the end of the range and returns a new logical-end iterator. The actual deletion is done by erase. Without knowing this, the container's size does not shrink and the elements that were logically removed remain at the back in a valid but unspecified (or moved-from) state.

C++
// Before C++20: Standard Idiom
v.erase(
    std::remove_if(v.begin(), v.end(),
                   [](int x) { return x < 0; }),
    v.end());

// After C++20: Shorthand for the Same Behavior
std::erase_if(v, [](int x) { return x < 0; });

Node-based containers such as std::list have a member function remove_if that performs true deletion. The behavior differs by container type, which is a common pitfall.


Appendix: Language features often confused with idioms

These two are not named idioms; they are C++11 language features. Idioms are the patterns that use these features safely.

range-based for

C++11 syntax. Iterates over a collection without explicit iterators. More of a recommended usage than a named idiom.13

C++
for (const auto& item : container) { /* ... */ }
for (auto& item : container)       { /* Mutable */ }

Related idiom: When an index is needed, C++23's std::views::enumerate is an option, though standard library support varies by environment. Where support is lacking, a separate counter variable, range-v3, or a zip view combined with iota are common alternatives.

move semantics

A C++11 language feature. Transfers resources instead of copying them using lvalue/rvalue references and std::move. The mechanism itself is not an idiom.14

Related idioms:

  • Rule of Five: Designing movable classes

  • Move-only types: Prohibiting copy and allowing only move, as with std::unique_ptr

  • Sink parameter: Taking an argument by value and moving from it internally, letting the caller decide whether to copy or move

  • Perfect forwarding: Preserving the value category of an argument using std::forward<T>

  • noexcept move: Standard containers will only adopt a move operation if it is marked noexcept

C++
// Sink Parameter Idiom
class Builder
{
public:
    void set_name(std::string n)   // Pass by value
    {
        name = std::move(n);       // Move internally
    }
private:
    std::string name;
};

Summary

Category

Idiom

Resource management

RAII, Rule of Zero/Three/Five, Smart pointer ownership, copy-and-swap

Encapsulation

Pimpl, NVI

Type representation

std::optional, std::variant + std::visit

Compile-time

CRTP, Type erasure, EBO

STL

Erase-remove

(Language features)

range-based for, move semantics


Using complex tools does not deepen a person. Sometimes it only makes them more practiced at rationalizing complex tools.

Personal note: The C++ community has an unusually strong sense of superiority despite having too many versions, a complex standard, and a fragmented ecosystem. There is a strange mixture of pride in low-level control and contempt for modern safety and productivity.

Honestly, it is not a culture I am eager to get close to. If low-level control really is the purpose of one's life, it seems more consistent to just write assembly.



Footnotes

  1. RAII. Bjarne Stroustrup established Resource Acquisition Is Initialization as C++'s resource management principle. - cppreference: RAII - The foundational convention: constructors acquire resources and establish all class invariants, while destructors release resources and never throw exceptions - Wikipedia: Resource acquisition is initialization - Originated in C++; related concepts are also used in Ada, Vala, and Rust
  2. Rule of Zero / Three / Five. Marshall Cline first proposed the Rule of Three in 1991. - cppreference: Rule of three/five/zero - C++ Core Guidelines C.20 (Rule of Zero) - C++ Core Guidelines C.21 (Rule of Five) - ACCU Overload #120: Rule of Zero/Three/Five History - Traces the evolution from Stroustrup through Sutter/Alexandrescu to Walter Brown's N3839 - Fluent C++: The Rule of Zero - Includes Scott Meyers' counterargument on "Rule of Five Defaults"
  3. Smart pointer ownership. - C++ Core Guidelines R.20–R.34 (Resource Management) - C++ Core Guidelines F.7 - Guideline to accept non-owning parameters as raw pointers or references - cppreference: std::unique_ptr - cppreference: std::shared_ptr - cppreference: std::weak_ptr
  4. Copy-and-swap. Herb Sutter's Guru of the Week #59 (1999) is the oldest systematic reference. - GotW #59: Exception-Safe Class Design, Part 1 - Stack Overflow: What is the copy-and-swap idiom? - Community standard explanation - Mropert: copy-and-swap, 20 years later - Clarifies the cost of strong exception guarantees and tensions with move semantics
  5. Pimpl (Pointer to Implementation). Herb Sutter's GotW series is the canonical source. - GotW #24: Compilation Firewalls - Original - GotW #100: Compilation Firewalls (C++11) - Evaluates design choices for how much private data and member functions to place in the implementation - GotW #101: Compilation Firewalls, Part 2 - C++ Stories: The Pimpl Pattern - Clarifies synonyms: d-pointer, compiler firewall, Cheshire Cat, and opaque pointer
  6. NVI (Non-Virtual Interface). Herb Sutter published this in the C/C++ Users Journal in 2001. - Sutter: Virtuality - The most influential article explicitly recommending NVI - C++ Core Guidelines C.133: Avoid protected data
  7. std::optional. - cppreference: std::optional - C++ Core Guidelines F.60 - Context for preferring optional over pointers - cppreference: std::expected (C++23) - A richer alternative
  8. std::variant + std::visit. - cppreference: std::variant - cppreference: std::visit - C++ Core Guidelines C.181: Avoid naked unions - Recommends variant over raw unions
  9. CRTP (Curiously Recurring Template Pattern). James O. Coplien coined the term in the C++ Report in 1995. - Coplien (1995): Curiously Recurring Template Patterns (PDF) - Original source - Wikipedia: Curiously recurring template pattern - A form of F-bound polymorphism, widely used in Windows ATL/WTL - Fluent C++: The Curiously Recurring Template Pattern - Practical tutorial series
  10. Type erasure. Sean Parent's GoingNative 2013 presentation was a watershed moment. - Microsoft Learn: Inheritance Is The Base Class of Evil - Demonstrates non-invasive runtime polymorphism based on value semantics - YouTube: Sean Parent - Inheritance Is The Base Class of Evil - GitHub gist: martinmoene/cd3286daa799acc55cc0 - Code examples from the presentation - Alexandrescu, Andrei. Modern C++ Design, "Type Erasure" chapter - Iglberger, Klaus. Breaking Dependencies: Type Erasure - A Design Analysis (CppCon)
  11. EBO (Empty Base Optimization). - cppreference: Empty base optimization - [cppreference: [[nouniqueaddress]] (C++20)](https://en.cppreference.com/cpp/language/attributes/nouniqueaddress) - Modern alternative for members - Boost: compressed_pair - Practical application of EBO
  12. Erase-remove idiom. - cppreference: std::remove, std::remove_if - Includes explanation of why it does not simply delete - cppreference: std::erase, std::erase_if (C++20) - Meyers, Scott. Effective STL, Item 32: "Follow remove-like algorithms by erase if you really want to remove something"
  13. Range-based for. - cppreference: Range-based for loop - C++ Core Guidelines ES.71 - Prefers range-for over traditional for loops
  14. Move semantics. - cppreference: Move constructors - C++ Core Guidelines F.18 (sink parameter) - Meyers, Scott. Effective Modern C++, Items 23–30 (rvalue references, move, and forwarding)
C++ Idioms