12 Best Practices and Common Idioms

You now have a solid foundation in C++: classes, templates, the standard library, concurrency, and more. Knowing the language features is necessary, but knowing how to use them well is what separates a competent programmer from a great one. This chapter covers the coding standards, idioms, and patterns that experienced C++ developers rely on, and closes with a look at what is coming in C++26.

12.1 Coding Standards and Style

12.1.1 Naming Conventions

There is no single naming convention in C++. The standard library uses snake_case for everything. Google style uses CamelCase for types and functions, and snake_case for variables. The most important rule is consistency within your project:

Element Common styles
Types / classes PascalCase or snake_case
Functions snake_case or camelCase
Variables snake_case
Constants kConstantName or UPPER_CASE
Private members name_ (trailing underscore)
Macros UPPER_CASE (the one universal convention)

12.1.2 Const-Correctness

Mark everything const that should not change. This communicates intent, catches bugs at compile time, and enables optimizations:

// Parameters
void process(const std::string& name);    // does not modify name
void modify(std::string& name);           // may modify name

// Member functions
class Playlist {
public:
    int size() const;                     // does not modify the object
    void add(const std::string& song);    // modifies the object
};

// Variables
const int max_tracks = 100;

Tip: Make everything const by default. Only remove const when you have a reason to mutate. This is the opposite of what most beginners do, and it prevents a large class of bugs.

12.1.3 The Rule of Zero

If your class does not manage resources directly (it uses std::string, std::vector, smart pointers, etc.), you should not write any special member functions — the compiler-generated defaults do the right thing:

#include <string>
#include <utility>

class Song {
public:
    Song(std::string title, std::string artist)
        : title_(std::move(title)), artist_(std::move(artist)) {}

    // No destructor, no copy/move constructors, no assignment operators.
    // The compiler generates correct versions automatically.

private:
    std::string title_;
    std::string artist_;
};

This is the Rule of Zero: let the compiler do the work.

12.1.4 The Rule of Five

If your class manages a resource directly (raw pointers, file handles, etc.), you must define all five special member functions:

  1. Destructor
  2. Copy constructor
  3. Copy assignment operator
  4. Move constructor
  5. Move assignment operator
#include <cstring>

class Buffer {
public:
    explicit Buffer(size_t size) : data_(new char[size]), size_(size) {}

    ~Buffer() { delete[] data_; }

    Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
        std::memcpy(data_, other.data_, size_);
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            char* new_data = new char[other.size_];  // allocate first...
            std::memcpy(new_data, other.data_, other.size_);
            delete[] data_;                          // ...then release the old buffer
            data_ = new_data;
            size_ = other.size_;
        }
        return *this;
    }

    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

private:
    char* data_;
    size_t size_;
};

Note the order in the copy assignment: allocate the new buffer first, then delete the old one. If you delete first and new throws, data_ is left dangling and the destructor frees it a second time.

Tip: If you find yourself writing the Rule of Five, ask whether a standard type (like std::vector<char>) could manage the resource for you instead, bringing you back to the Rule of Zero.

12.1.5 When to Use auto

auto deduces the type from the initializer. Use it when the type is obvious or verbose, not when it obscures meaning:

// Good: type is clear from context
auto it = my_map.find("key");
auto [name, score] = get_result();
auto ptr = std::make_unique<Widget>();

// Bad: what type is this?
auto x = compute();          // unclear --- reader must find compute()
auto result = process(data); // unclear

12.2 Common C++ Idioms

12.2.1 PIMPL (Pointer to Implementation)

The PIMPL idiom hides implementation details behind a pointer, reducing compile-time dependencies. Changes to the implementation do not require recompiling users of the class:

// playlist.h
#include <memory>
#include <string>

class Playlist {
public:
    Playlist();
    ~Playlist();
    void add(const std::string& song);
    int size() const;

private:
    struct Impl;               // forward declaration
    std::unique_ptr<Impl> pimpl_;
};
// playlist.cpp
#include "playlist.h"
#include <vector>

struct Playlist::Impl {
    std::vector<std::string> songs;
};

Playlist::Playlist() : pimpl_(std::make_unique<Impl>()) {}
Playlist::~Playlist() = default;

void Playlist::add(const std::string& song) {
    pimpl_->songs.push_back(song);
}

int Playlist::size() const {
    return static_cast<int>(pimpl_->songs.size());
}

The header does not include <vector>, so files that include playlist.h do not depend on <vector>. This can significantly reduce build times in large projects.

Wut: The destructor must be defined in the .cpp file (even as = default) because unique_ptr needs the complete type to destroy it. If you let the compiler generate the destructor in the header, it will fail because Impl is incomplete there.

12.2.2 CRTP (Curiously Recurring Template Pattern)

The CRTP provides static polymorphism — polymorphic behavior resolved at compile time instead of run time:

#include <iostream>

template<typename Derived>
class Player {
public:
    void play() {
        static_cast<Derived*>(this)->play_impl();
    }
};

class MP3Player : public Player<MP3Player> {
public:
    void play_impl() {
        std::cout << "Playing MP3\n";
    }
};

class WAVPlayer : public Player<WAVPlayer> {
public:
    void play_impl() {
        std::cout << "Playing WAV\n";
    }
};

template<typename T>
void start_playback(Player<T>& player) {
    player.play();
}

int main() {
    MP3Player mp3;
    WAVPlayer wav;
    start_playback(mp3);  // Playing MP3
    start_playback(wav);  // Playing WAV

    return 0;
}
Playing MP3
Playing WAV

Unlike virtual functions, CRTP has no vtable overhead. The compiler inlines the call because it knows the exact type at compile time.

The trade-off: you cannot store different Player types in the same container (no common base class), so CRTP does not replace virtual functions when you need run-time polymorphism (Chapter 1).

12.2.3 Tag Dispatch

Tag dispatch selects a function overload at compile time using empty tag types:

#include <iostream>

struct fast_tag {};
struct safe_tag {};

void process(int x, fast_tag) {
    std::cout << "Fast path: " << x << "\n";
}

void process(int x, safe_tag) {
    if (x < 0) {
        std::cout << "Error: negative\n";
        return;
    }
    std::cout << "Safe path: " << x << "\n";
}

int main() {
    process(42, fast_tag{});
    process(-1, safe_tag{});

    return 0;
}
Fast path: 42
Error: negative

Tag dispatch was more common before C++20. Today, if constexpr and concepts (Chapter 2) often achieve the same result more cleanly.

12.3 Code Review Checklist

When reviewing your own or others’ C++ code, check for:

Correctness

Resource Management

Safety

Performance

Style

12.4 What’s Next: C++26 Preview

C++ continues to evolve. Here are some features expected in C++26 and beyond:

12.4.1 std::execution (Senders/Receivers)

A structured framework for asynchronous and parallel programming, replacing ad-hoc thread management with composable operations.

12.4.2 Pattern Matching (post-C++26)

A cleaner alternative to if/else chains and std::visit, still under active development:

// Proposed syntax (not final)
inspect (value) {
    0 => std::cout << "zero\n";
    int i if i > 0 => std::cout << "positive\n";
    _ => std::cout << "other\n";
};

12.4.3 Reflection

The ability to inspect types at compile time — querying member names, types, and attributes programmatically. This will enable automatic serialization, ORM generation, and much more.

12.4.4 Contracts

Preconditions and postconditions as part of function declarations:

int sqrt_of(int x)
    pre (x >= 0)
    post (r: r >= 0) {
    // ...
}

Tip: C++ evolves roughly every three years. Keeping up with the latest standards ensures you can write cleaner, safer code. Follow the ISO C++ committee’s progress and experiment with new features as your compiler supports them.

12.5 Try It: A CRTP Instance Counter

Here is a program that uses the CRTP to count how many instances of each class are alive. Because each derived class instantiates its own Counted<Derived>, each class gets its own independent counter. Type it in, compile with g++ -std=c++23, and experiment:

#include <iostream>

template<typename Derived>
class Counted {
public:
    Counted() { ++count_; }
    Counted(const Counted&) { ++count_; }
    ~Counted() { --count_; }

    static int alive() { return count_; }

private:
    static inline int count_ = 0;
};

class Window : public Counted<Window> {};
class Socket : public Counted<Socket> {};

int main() {
    Window w1, w2;
    std::cout << "Windows alive: " << Window::alive() << "\n";

    {
        Socket s;
        Window w3 = w1;  // copies are counted too
        std::cout << "Windows alive: " << Window::alive() << "\n";
        std::cout << "Sockets alive: " << Socket::alive() << "\n";
    }   // s and w3 are destroyed here

    std::cout << "Windows alive: " << Window::alive() << "\n";
    std::cout << "Sockets alive: " << Socket::alive() << "\n";

    return 0;
}
Windows alive: 2
Windows alive: 3
Sockets alive: 1
Windows alive: 2
Sockets alive: 0

Try removing the copy constructor and rerun — the final Window count drops to 1 even though w1 and w2 are both still alive. Can you explain why? Try adding a third counted class and confirm its counter is independent of the other two. Try storing some Window objects in a std::vector and watch the count as the vector grows and is destroyed.

12.6 Key Points

  • Naming conventions vary; consistency within a project matters most.
  • Const-correctness: make everything const by default.
  • Rule of Zero: if your class does not manage resources directly, do not write special member functions.
  • Rule of Five: if you manage a resource directly, define all five special members.
  • Use auto when the type is clear or verbose, not when it obscures meaning.
  • PIMPL hides implementation details behind a pointer, reducing compile dependencies and build times. The destructor must be in the .cpp file.
  • CRTP provides static polymorphism with no vtable overhead, but does not support run-time polymorphism.
  • Tag dispatch selects overloads at compile time; largely superseded by if constexpr and concepts.
  • A good code review checks correctness, resource management, safety, performance, and style.
  • C++26 will bring std::execution, reflection, and contracts; pattern matching is expected in a later standard.

12.7 Exercises

  1. Think about it: Why does the text recommend starting with const and removing it only when needed, rather than the other way around?

  2. Think about it: When should you follow the Rule of Zero vs. the Rule of Five? How do you decide?

  3. Where is the bug?

    class Data {
    public:
        Data(int n) : ptr_(new int[n]), size_(n) {}
        ~Data() { delete[] ptr_; }
    private:
        int* ptr_;
        int size_;
    };
    
    Data a(10);
    Data b = a;
  4. Think about it: What are the trade-offs between CRTP (static polymorphism) and virtual functions (dynamic polymorphism)? When would you choose each?

  5. What does this print?

    auto x = 42;
    auto y = 3.14;
    auto z = x + y;
    std::cout << z << "\n";
  6. Think about it: Why must the PIMPL destructor be defined in the .cpp file? What error do you get if you let the compiler generate it in the header?

  7. Where is the problem?

    void add_to_vector(std::vector<int> v, int x) {
        v.push_back(x);
    }
    
    std::vector<int> data = {1, 2, 3};
    add_to_vector(data, 4);
    std::cout << data.size() << "\n";
  8. Think about it: The code review checklist mentions checking that comments explain “why” not “what.” Why is this distinction important? Give an example of a bad comment and a good one for the same code.

  9. Calculation: A class has a std::vector<std::string> member and a std::unique_ptr<Widget> member. How many special member functions should you write for this class?

  10. Write a program that uses the CRTP to create a Printable<Derived> base class with a print() method that calls Derived::to_string(). Create two derived classes (e.g., Song and Album) that each implement to_string(). Demonstrate calling print() on instances of each.