Chapter 33: Style Guides and Best Practices
Style is the invisible scaffolding that holds a codebase together. A consistent set of rules reduces mental load, lowers friction in reviews, and helps new contributors match the project’s voice. This chapter collects pragmatic guidance that you can adopt as is or adapt to your team’s needs. Favor rules that prevent bugs and smooth collaboration; avoid rules that only satisfy taste.
Naming, layout, and comments
Names are contracts. Choose names that tell the reader what a thing does, not how it is implemented. Keep layout calm and predictable so that change diffs are small and easy to read. Use comments to explain the why; let code show the how.
Naming rules
Use lowercase_with_underscores for functions and variables, PascalCase for types and concepts, and ALL_CAPS for macros. Prefer verbs for functions and nouns for types. Keep abbreviations rare and obvious (id, URL, CPU). Avoid negative booleans because they invert logic twice in conditions.
struct FileReader { /* ... */ };
auto read_file(std::string_view path) -> std::expected<std::string, std::error_code>;
bool is_ready = true; // not: ready_flag, not_ready
Layout and whitespace
Keep lines under a sensible limit, for example 100 or 120 characters. Break long template parameters and argument lists one per line with trailing commas. Indent with spaces consistently. Place braces in a style that your team agrees on, then stick to it.
auto make_map() -> std::unordered_map<std::string, std::vector<int>> {
return {
{"alpha", {1, 2, 3}},
{"beta", {4, 5, 6}},
};
}
Commenting guidelines
Write API comments for public interfaces and tricky invariants. Avoid comments that restate the code. Use block comments for high level notes and short end of line comments for delicate conditions. Keep a changelog of design decisions in markdown rather than burying history inside functions.
Header hygiene and #include order
Headers shape build performance and binary hygiene. A clean header includes only what it needs, exposes minimal surface area, and compiles on its own. Include order affects reproducibility and reveals hidden dependencies.
Self containment
Every public header must compile on its own when placed in an empty translation unit. Forward declare where possible to avoid dragging in heavy transitive headers. Prefer including headers in source files rather than headers that include other headers.
// good: forward declaration is enough
namespace foo { class Widget; }
void use_widget(const foo::Widget&);
// needs full definition, so include in the .cpp
// #include "foo/widget.hpp"
#include order
Use a consistent order: the header’s own header first when in a source file, then related project headers, then third party headers, then standard library headers. Separate groups with a blank line. This exposes missing includes quickly and keeps diffs readable.
// in src/bar.cpp
#include "bar.hpp"
#include "core/util.hpp"
#include <fmt/format.h>
#include <vector>
#include <string>
Guards and visibility
Use #pragma once when available, otherwise classic include guards. Do not put non inline variables in headers. Keep inline functions tiny and stable to avoid code bloat across translation units.
API design principles
Good APIs feel obvious once seen. They prefer value semantics, express clear ownership, and fail in predictable ways. When in doubt, start with the simplest shape that solves a real user task and expand only when real use cases require it.
Surface area and stability
Keep the public surface small. Hide helpers in an internal namespace or in private modules. Mark unstable parts as experimental in names or in documentation. Once released, evolve by addition and deprecation, not by surprise removal.
Ownership and parameters
Use std::string_view for read only string parameters, use std::span<T> for contiguous ranges, and return owning values where practical. Accept sinks by template or concept so callers can pass vectors, back inserters, or streams without friction.
auto tokenize(std::string_view s, char delim) -> std::vector<std::string>;
auto write_all(std::span<const std::byte> data, auto& sink) -> std::error_code;
Errors and contracts
Document preconditions and postconditions in a single sentence near each function. For recoverable problems, use std::expected or std::error_code. Reserve exceptions for programming errors and rare failures where unwinding to a safe boundary improves clarity.
Safety first defaults
Prefer patterns that prevent bugs by default. Make it hard to use APIs incorrectly. Choose types that encode intent and lifetimes so that mistakes are caught at compile time.
Initialize everything
Use brace initialization for objects and members. Default construct with valid states. Mark constructors explicit when a single argument could surprise readers. Prefer enum class to plain enums so that scopes remain clear.
struct Config {
int port{0};
std::string host{"localhost"};
std::chrono::milliseconds timeout{1000};
explicit Config(int p) : port{p} {}
};
Constrain templates
Add concept constraints to templates so that misuse stops at the call site. Lean on standard concepts such as std::ranges::range, std::regular, and std::invocable. Provide static asserts with friendly messages for edge cases that concepts do not express well.
template<std::ranges::range R, std::invocable<std::ranges::range_value_t<R>> F>
auto map(R&& r, F f) {
/* … */
}
Immutable by default
Pass inputs as const& or views and return new values instead of mutating in place unless profiling proves otherwise. Keep shared ownership rare; prefer unique ownership with clear transfer points. Mark functions noexcept only when you can uphold the guarantee.
Code review checklists
Reviews catch defects, spread knowledge, and keep the style steady. A short checklist turns fuzzy goals into concrete checks that teams can apply quickly. Keep the list brief so that it is used every time.
Suggested checklist
Embed this list in your pull request template and adapt it as your project grows.
| Area | Checks |
| Naming | Do names describe purpose; are types and functions consistent with the guide |
| Ownership | Are lifetimes clear; are unique_ptr and span used where appropriate |
| Errors | Are failures reported with std::expected or std::error_code; are contracts documented |
| Headers | Does each header compile alone; are includes minimal and ordered; no hidden dependencies |
| APIs | Is the surface minimal; are parameters expressive; is behavior testable |
| Safety | Are objects initialized; are templates constrained; are conversions explicit |
| Perf | Any obvious needless copies; hot paths measured; allocations visible |
| Docs | Comments explain intent; public functions have one sentence summaries |
| Build | No new warnings; CI passes; tools such as formatters and linters run clean |
Review tone and process
Keep feedback specific and kind. Point at code and outcomes rather than people. Suggest concrete changes with short rationale. Small focused pull requests review faster and merge with fewer conflicts, so prefer incremental changes over large surprise drops.