Skip to content

C++ Coding Guidelines

Compiler

  • Clang preferred; GCC acceptable.
  • debug-build: -Wall -Wextra -Wpedantic -Werror -g -O0 -fsanitize=address,undefined
  • release-build: -O3 -DNDEBUG — no sanitizers

Dialect (C++23)

  • Trailing return type syntax: auto foo() -> int. Exception: a header shared with C (e.g. app_c_api.h) stays plain C syntax throughout. Trailing return types and () for no-args (vs. (void)) are C++-only features that would fail to compile as C.
  • const/constexpr on every variable and argument where possible.
  • Uniform/direct-list-initialization: int x{42}.
  • if/while with initializer: if (const auto value{expr()}) { ... } — combines const-everywhere and uniform-init; the standard form for checking an std::optional/std::expected/pointer result.
  • Range-for over index loop; auto for type inference.
  • enum class over plain enum; static_cast over C-style casts; nullptr over NULL.
  • No macros (use constexpr/inline instead).
  • Digit separators for readability: 5'000.
  • Smart pointers over raw pointers.
  • End an otherwise-exhaustive switch over an enum class with std::unreachable() after the switch, never a default: case. A default: case would silently swallow a future added enumerator instead of failing to compile. GCC's -Wreturn-type doesn't treat full enumerator coverage as provably exhaustive, unlike Clang, and warns without a trailing return path; std::unreachable() keeps the build clean under both compilers.

Naming

  • snake_case — variables, members, functions.
  • Snake_Case — types (classes, structs, enums).
  • SCREAMING_SNAKE_CASE — macros, pin constants.
  • m_ prefix for private members.
  • Functions with side effects: verbs (send, save). Pure functions: noun (payload, hex).
  • Predicates: is_/has_ prefix.

Namespace

  • All code in a namespace matching the project name (snake_case). Nested by layer: <project>::domain, <project>::usecase, <project>::infra.
  • Never using namespace — anywhere, for any namespace (std or third-party): defeats the point of namespacing and risks silent ADL surprises as either side gains new names.
  • The same ban applies to a using-declaration (using std::X;) at namespace scope in a header — it leaks into every translation unit that includes the header, the same pollution using namespace causes. It's only safe where it doesn't leak: inside a function or class body defined in the header (e.g. an inline function). This is a scope rule, not a header-vs-.cpp rule.
  • Third-party namespaces (fmt::, leveldb::, ...) always stay fully qualified at the call site — the reader isn't as familiar with them as with std, so the prefix carries real information.
  • std:: may be shortened with a targeted using std::string;-style declaration — in a .cpp file, or inside a header's function/class body per the rule above — when it measurably improves readability (e.g. a functional style with frequent calls to the same few algorithms). Scope it as tightly as the call sites allow (a single function over file scope), and choose names deliberately: prefer this only where collision with a project type or a future standard-library addition is unlikely; qualify fully when in doubt. Embedded targets are stricter — see cpp_embedded_coding.md.

Types

  • struct for plain data; class for types with invariants.
  • using aliases instead of typedef.
  • Fixed-width integers: uint8_t, int16_t, uint32_t.
  • Use type aliases in domain logic to improve type safety: using Amount = float;, using Order_Number = std::string;.

Error Handling

  • C++ has no compiler-enforced exception specifications (removed entirely in C++17) — nothing forces or even hints to a caller which exceptions a function might throw. The "self-documenting, catch-dispatchable API surface" benefit that justifies many fine-grained exception classes in Java mostly disappears here.
  • Prefer a small number of exception types (often one per module/domain), each carrying an error-code enum or similar plus context data as members — not one class per error condition.
  • For expected/recoverable failures, prefer std::expected<T, E> (C++23) or std::error_code (<system_error>) over throwing; reserve throw for genuinely exceptional, unrecoverable conditions.
  • Whichever mechanism is used, expose the failure's data via accessible members — not only via what().
  • Embedded targets: no exceptions at all — see cpp_embedded_coding.md.

[[nodiscard]] & Return Values

  • If silently discarding a function's return value would be a bug, then annotate that function. Applies to public or private, member or free functions alike. Returning error/status results, std::optional/std::expected used for control flow, or resource handles is a good indicator for [[nodiscard]].
  • Skip it only where discarding truly carries no risk (e.g. a pure getter read purely for its side-effect-free informational value).
  • Always evaluate return values unless marked [[maybe_unused]].
  • Use std::ignore if the return value of a [[nodiscard]] function is intentionally ignored.

Headers

  • #pragma once in every header.
  • Single-line getters/setters: inline in class body. Multi-line inline: inline below class. Everything else: .cpp.

Includes

Include What You Use (IWYU).

CMake

  • One target per logical component (add_library / add_executable).
  • Only target-scoped commands: target_include_directories, target_link_libraries, target_compile_options — never global variants.
  • Build directory layout:
  • Libraries use cmake_layout — CMake places artefacts under build/Debug/ and build/Release/. Required for multi-config Conan packaging (make package).
  • Services and CLI tools use a flat build/ — one active build type at a time, sufficient for containerised executables.
  • When switching between make build (Debug) and make release-build (Release) locally, run make clean first to avoid stale object files from the previous configuration.

OOP

  • Mark all overriding methods override; never repeat virtual in derived classes.
  • Mark leaf classes final.

Debugging

  • Wrap debug output in #ifdef DEBUG blocks.

Testing (Catch2)

  • REQUIRE for preconditions, CHECK for assertions.
  • Structure test bodies with // GIVEN, // WHEN, // THEN.
  • Test files in test/, named <class_name>_test.cpp, mirroring the layer subdirectory the class under test lives in (src/usecase/foo.hpp → test/usecase/foo_test.cpp); a test double used only within one layer's tests lives under that layer's test/<layer>/mocks/.
  • BDD tests with Catch2; no special BDD framework unless stated otherwise.

Acceptance Tests (pytest-bdd)

C++ has no maintained native Gherkin runner (cucumber-cpp has been effectively unmaintained for years). Behave was the first choice but cannot decouple feature-file location from step-definition location (it discovers steps/ by walking upward from the feature file's own directory), which is incompatible with reading scenarios straight from test/bdd/features/. pytest-bdd natively separates the two (scenarios(...) in a test module anywhere, steps as plain @given/@when/@then functions in that same module) and drives a coverage-instrumented build (make bdd-test) instead of a native equivalent — over HTTP against a running instance for services, or via a subprocess invocation per scenario for CLI tools and libraries (a thin CLI-shim wraps a library's public API for this purpose — see library_design.md). Catch2 remains for C++ unit/component tests that need no live process — the two are complementary: Catch2 tests objects directly, pytest-bdd exercises the same binary the way a real caller would.

Directives

  • C++23; trailing return types (auto foo() -> int); constexpr wherever possible. An extern "C" header shared with C code stays plain C
  • Brace initialization (int x{42}); range-for over index loops; auto for type inference
  • if and while with initializer for optional, expected, and pointer checks
  • enum class, static_cast, nullptr, using aliases; smart pointers over raw pointers
  • End an exhaustive switch over an enum class with std::unreachable() after it, never with default:
  • Verbs for functions with side effects, nouns for pure ones, is_/has_ for predicates
  • All code in the project's namespace; never using namespace, and no namespace-scope using in a header; qualify third-party namespaces fully
  • struct for plain data, class for types with invariants
  • Fixed-width integers; type aliases for domain type safety (using Order_Number = std::string;)
  • #pragma once in every header; include what you use
  • One-line accessors inline in the class body, longer inline functions below it, the rest in .cpp
  • One CMake target per component; target-scoped commands only
  • [[nodiscard]] on every function whose result would be a bug to discard; use the result or std::ignore it
  • No macros; use constexpr or inline
  • Few exception types, or std::expected, carrying an error code and context as members
  • override on overriding methods, never a repeated virtual; leaf classes are final
  • Debug-only output inside #ifdef DEBUG
  • Catch2 tests: REQUIRE for preconditions, CHECK for assertions, bodies structured by // GIVEN, // WHEN, // THEN
  • Test files test/<layer>/<class_name>_test.cpp; a mock used by one layer lives in test/<layer>/mocks/