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/constexpron every variable and argument where possible.- Uniform/direct-list-initialization:
int x{42}. if/whilewith initializer:if (const auto value{expr()}) { ... }— combines const-everywhere and uniform-init; the standard form for checking anstd::optional/std::expected/pointer result.- Range-for over index loop;
autofor type inference. enum classover plainenum;static_castover C-style casts;nullptroverNULL.- No macros (use
constexpr/inlineinstead). - Digit separators for readability:
5'000. - Smart pointers over raw pointers.
- End an otherwise-exhaustive
switchover anenum classwithstd::unreachable()after theswitch, never adefault:case. Adefault:case would silently swallow a future added enumerator instead of failing to compile. GCC's-Wreturn-typedoesn'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 (stdor 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 pollutionusing namespacecauses. 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-.cpprule. - Third-party namespaces (
fmt::,leveldb::, ...) always stay fully qualified at the call site — the reader isn't as familiar with them as withstd, so the prefix carries real information. std::may be shortened with a targetedusing std::string;-style declaration — in a.cppfile, 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 — seecpp_embedded_coding.md.
Types¶
structfor plain data;classfor types with invariants.usingaliases instead oftypedef.- 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) orstd::error_code(<system_error>) over throwing; reservethrowfor 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::expectedused 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::ignoreif the return value of a[[nodiscard]]function is intentionally ignored.
Headers¶
#pragma oncein every header.- Single-line getters/setters: inline in class body. Multi-line inline:
inlinebelow 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 underbuild/Debug/andbuild/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) andmake release-build(Release) locally, runmake cleanfirst to avoid stale object files from the previous configuration.
OOP¶
- Mark all overriding methods
override; never repeatvirtualin derived classes. - Mark leaf classes
final.
Debugging¶
- Wrap debug output in
#ifdef DEBUGblocks.
Testing (Catch2)¶
REQUIREfor preconditions,CHECKfor 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'stest/<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);constexprwherever possible. Anextern "C"header shared with C code stays plain C - Brace initialization (
int x{42}); range-for over index loops;autofor type inference ifandwhilewith initializer for optional, expected, and pointer checksenum class,static_cast,nullptr,usingaliases; smart pointers over raw pointers- End an exhaustive
switchover anenum classwithstd::unreachable()after it, never withdefault: - 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-scopeusingin a header; qualify third-party namespaces fully structfor plain data,classfor types with invariants- Fixed-width integers; type aliases for domain type safety (
using Order_Number = std::string;) #pragma oncein 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 orstd::ignoreit- No macros; use
constexprorinline - Few exception types, or
std::expected, carrying an error code and context as members overrideon overriding methods, never a repeatedvirtual; leaf classes arefinal- Debug-only output inside
#ifdef DEBUG - Catch2 tests:
REQUIREfor preconditions,CHECKfor assertions, bodies structured by// GIVEN,// WHEN,// THEN - Test files
test/<layer>/<class_name>_test.cpp; a mock used by one layer lives intest/<layer>/mocks/