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.
Coding Standards and Style
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 snake_case for functions. 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) |
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.
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:
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.
The Rule of Five
If your class manages a resource directly (raw pointers, file handles, etc.), you must define all five special member functions:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
#include <cstring>
class Buffer {
public:
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) {
delete[] data_;
data_ = new char[other.size_];
size_ = other.size_;
std::memcpy(data_, other.data_, 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_;
};
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.
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); // unclearCommon C++ Idioms
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.
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 WAVUnlike 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).
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: negativeTag dispatch was more common before C++20. Today, if constexpr and concepts (Chapter 2) often achieve the same result more cleanly.
Code Review Checklist
When reviewing your own or others’ C++ code, check for:
Correctness
Resource Management
Safety
Performance
Style
What’s Next: C++26 Preview
C++ continues to evolve. Here are some features expected in C++26 and beyond:
std::execution (Senders/Receivers)
A structured framework for asynchronous and parallel programming, replacing ad-hoc thread management with composable operations.
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";
};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.
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.
Key Points
- Naming conventions vary; consistency within a project matters most.
- Const-correctness: make everything
constby 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
autowhen 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
.cppfile. - 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 constexprand concepts. - A good code review checks correctness, resource management, safety, performance, and style.
- C++26 will bring
std::execution, pattern matching, reflection, and contracts.
Exercises
Think about it: Why does the text recommend starting with
constand removing it only when needed, rather than the other way around?Think about it: When should you follow the Rule of Zero vs. the Rule of Five? How do you decide?
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;Think about it: What are the trade-offs between CRTP (static polymorphism) and virtual functions (dynamic polymorphism)? When would you choose each?
What does this print?
auto x = 42; auto y = 3.14; auto z = x + y; std::cout << z << "\n";Think about it: Why must the PIMPL destructor be defined in the
.cppfile? What error do you get if you let the compiler generate it in the header?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";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.
Calculation: A class has a
std::vector<std::string>member and astd::unique_ptr<Widget>member. How many special member functions should you write for this class?Write a program that uses the CRTP to create a
Printable<Derived>base class with aprint()method that callsDerived::to_string(). Create two derived classes (e.g.,SongandAlbum) that each implementto_string(). Demonstrate callingprint()on instances of each.