<< Home

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
💡 When a name feels hard to choose, the design may be vague. Sketch the public usage first, then name the pieces to fit that sketch.

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.

⚠️ Do not let comments drift. When code changes, update or remove nearby comments in the same commit so that they remain trustworthy.

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.

💡 Inspect headers with tooling. Run include-what-you-use or similar linters in CI to prevent accidental dependencies from creeping in.

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.

⚠️ Never hide errors. If a function can fail, return a result that forces the caller to inspect success or failure, or make failure impossible by construction.

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.

💡 Combine small safe defaults into checklists in your repository so that newcomers learn the project’s habits without reading a long essay.

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.

AreaChecks
NamingDo names describe purpose; are types and functions consistent with the guide
OwnershipAre lifetimes clear; are unique_ptr and span used where appropriate
ErrorsAre failures reported with std::expected or std::error_code; are contracts documented
HeadersDoes each header compile alone; are includes minimal and ordered; no hidden dependencies
APIsIs the surface minimal; are parameters expressive; is behavior testable
SafetyAre objects initialized; are templates constrained; are conversions explicit
PerfAny obvious needless copies; hot paths measured; allocations visible
DocsComments explain intent; public functions have one sentence summaries
BuildNo 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.