C++ code style: clang-format configuration

Table of Contents

Summary

This file is the single authoritative source for .clang-format at the repository root. Every setting is accompanied by the reasoning behind its choice and a before/after example. Do not edit .clang-format directly — regenerate it with compass build --direct tangle_clang_format instead. The style is based on LLVM, tuned to match the conventions already in the codebase: 4-space indent, no namespace indentation, leading-comma constructor initialisers, and std → Boost → local include ordering. Return to Knowledge.

How to regenerate

Tangle the literate source to .clang-format and commit the result:

./compass.sh build --direct tangle_clang_format

This runs projects/ores.lisp/src/ores-build-clang-format.el in Emacs batch mode (no cmake/vcpkg), which calls org-babel-tangle-file on this document and writes the output to .clang-format at the repo root. The equivalent tangle_clang_format CMake target runs the same script in a full environment.

Literate source

The canonical .clang-format is assembled from the sections below. Each section documents one group of settings and contains the corresponding YAML fragment. The Emacs tangle script collects all #+begin_src yaml blocks and concatenates them into the output file.

File header

# -*- mode: yaml; yaml-tab-width: 4; indent-tabs-mode: nil -*-
# This file is generated from doc/knowledge/architecture/clang_format_config.org.
# Do not edit directly — run: ./compass.sh build --direct tangle_clang_format
---
Language: Cpp
# "Latest" means the most recent C++ standard the installed clang-format
# understands — effectively c++23 on current toolchains.
# The string "c++23" is not yet a valid enum value in clang-format 21.
Standard: Latest

Baseline style

LLVM style provides a well-documented, widely-used baseline. The sections below override specific settings to match the ORE Studio codebase conventions instead of LLVM's defaults.

# ── Baseline ─────────────────────────────────────────────────────────────────
BasedOnStyle: LLVM

Indentation

The project uses 4-space indentation throughout. Key deviations from LLVM defaults:

  • NamespaceIndentation: None — namespace body is not indented. LLVM defaults to None too, but we make it explicit. Before: namespace ores { class Foo {}; indented by 2 (LLVM default indent) After: namespace body at column 0.
  • AccessModifierOffset: -4public: / private: / protected: appear at the class indentation level, not further indented. Before: (LLVM default, -2 with 2-space indent) = public:= After: public: at column 0 relative to class.
  • IndentCaseLabels: truecase X: is indented relative to switch. Before: switch (x) { case 1: ... at same level as switch After: case 1: indented 4 spaces inside switch.
  • IndentPPDirectives: AfterHash — preprocessor directives are indented after the #, keeping nested #if/#endif readable. Before: #ifdef FOO\n#define BAR — all at column 0 After: #ifdef FOO\n# define BAR — nested defines indented
# ── Indentation ───────────────────────────────────────────────────────────────
IndentWidth: 4
TabWidth: 4
UseTab: Never
NamespaceIndentation: None
AccessModifierOffset: -4
IndentCaseLabels: true
IndentPPDirectives: AfterHash

Line length

100 characters. Long enough for deeply namespaced C++ and Qt signal connections without excessive wrapping; short enough to allow side-by-side diffs at reasonable terminal widths.

# ── Line length ───────────────────────────────────────────────────────────────
ColumnLimit: 100

Pointer and reference alignment

Pointer and reference qualifiers are attached to the type, not the variable name: T* x and T& x, not T *x and T &x. This is consistent throughout the codebase and matches Qt's own convention.

Before: void foo(const std::string &s, int *p) After: void foo(const std::string& s, int* p)

DerivePointerAlignment: false prevents clang-format from inferring the alignment from the existing code (which would be inconsistent during the transition period).

# ── Pointer / reference alignment ────────────────────────────────────────────
DerivePointerAlignment: false
PointerAlignment: Left
ReferenceAlignment: Left

Constructor initialiser lists

Initialisers use the leading-comma style: the colon and each comma appear at the start of a new line, indented 4 spaces. One initialiser per line, always (PackConstructorInitializers: Never).

Before (packed, trailing-comma):

Foo::Foo(int a, int b) : a_(a), b_(b), c_(0) {

After (one-per-line, leading-comma):

Foo::Foo(int a, int b)
    : a_(a)
    , b_(b)
    , c_(0) {

This style is already used consistently in the nats, http, and database components; the format pass extends it everywhere.

# ── Constructor initialiser lists ─────────────────────────────────────────────
BreakConstructorInitializers: BeforeComma
ConstructorInitializerIndentWidth: 4
PackConstructorInitializers: Never

Function parameters and arguments

Parameters and arguments are never bin-packed — they either all fit on one line or each goes on its own line, aligned to the opening parenthesis.

Before (bin-packed, fits whatever):

void foo(TypeA a, TypeB b,
         TypeC c, TypeD d) {

After (one per line when wrapping):

void foo(TypeA a,
         TypeB b,
         TypeC c,
         TypeD d) {
# ── Function parameters / arguments ──────────────────────────────────────────
BinPackParameters: false
BinPackArguments: false

Template declarations

The template<...> line is always on its own line, separate from the function or class declaration that follows. This improves readability for the long template parameter lists common in this codebase (repository patterns, codegen helpers, etc.).

Before:

template<typename T> void foo(T x) {

After:

template<typename T>
void foo(T x) {
# ── Templates ─────────────────────────────────────────────────────────────────
AlwaysBreakTemplateDeclarations: Yes

Short constructs

Only empty function bodies are allowed on a single line (e.g. Foo() {}). All other functions, including trivial one-liners, are expanded to multiple lines. Short lambdas may remain inline when they are the sole argument to a function call.

AllowShortIfStatementsOnASingleLine: Never and AllowShortLoopsOnASingleLine: false ensure that control flow is always on its own line, even for single-statement bodies — this makes diffs cleaner and reduces the chance of off-by-one errors when adding a second statement.

Before:

if (!joined.empty()) joined += ',';

After:

if (!joined.empty())
    joined += ',';
# ── Short constructs ──────────────────────────────────────────────────────────
AllowShortFunctionsOnASingleLine: Empty
AllowShortLambdasOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false

Include ordering

Includes are merged into a single sorted block (no blank lines between groups) and ordered local-first: project headers, then Qt, then Boost, then the standard library. Within each group, headers are sorted case-sensitively.

The ordering rationale: including local headers first is the Google and LLVM convention for a reason — it surfaces missing transitive includes immediately. If account.hpp forgets to include <string> but the #include "account.hpp" line comes after a <string> include, the bug is hidden. Local-first catches it because nothing has yet satisfied <string> when the header is parsed.

Before (unsorted, mixed):

#include <string>
#include <boost/uuid/uuid.hpp>
#include "ores.utility/uuid/tenant_id.hpp"
#include <chrono>

After (local → Qt → Boost → std):

#include "ores.utility/uuid/tenant_id.hpp"
#include <boost/uuid/uuid.hpp>
#include <chrono>
#include <string>

One genuine external constraint is carved out: a handful of Windows SDK headers (iphlpapi.h, ws2tcpip.h, psapi.h, tlhelp32.h, dbghelp.h, shellapi.h) are not self-sufficient — they require the types from windows.h to have been declared first, but do not include it themselves. That is Microsoft's defect, not ours, and no amount of code hygiene on our side can fix their headers. Pure alphabetical sorting puts iphlpapi.h before windows.h and breaks every Windows build (see the post-format CI failures in network_info.cpp). These headers therefore get a dedicated trailing category so they always sort after windows.h, which stays in the ordinary system group. Encoding the constraint here — rather than with clang-format off pragmas at the include sites — means the formatter itself produces the correct order and can never silently re-break it.

# ── Includes ──────────────────────────────────────────────────────────────────
# Sorted into one merged block (no blank line separators), ordered:
#   local "ores.*" / "ui_*" → Qt → Boost → std / other angle-bracket
#   → order-dependent Windows SDK headers
# Local-first ensures each header is self-sufficient: a missing transitive
# include cannot be silently satisfied by a preceding std/Boost header.
# The trailing Windows SDK category exists because those headers require
# windows.h first but do not include it themselves (a Microsoft defect);
# sorting them last guarantees windows.h precedes them.
SortIncludes: CaseSensitive
IncludeBlocks: Merge
IncludeCategories:
  - Regex: '^"'
    Priority: 1
  - Regex: '^<Q[A-Z]'
    Priority: 2
  - Regex: '^<boost/'
    Priority: 3
  - Regex: '^<(iphlpapi|ws2tcpip|psapi|tlhelp32|dbghelp|shellapi)\.h>'
    Priority: 5
  - Regex: '^<'
    Priority: 4

Miscellaneous

  • MaxEmptyLinesToKeep: 2 — prevents formatting passes from collapsing intentional visual groupings while still removing excessive blank lines.
  • FixNamespaceComments: false — the codebase uses bare } to close namespaces; adding trailing comments would be noisy.
  • SortUsingDeclarations: falseusing declarations are grouped intentionally; resorting them could change meaning for shadowing cases.
  • AlignEscapedNewlines: Left — backslash-escaped newlines in macros are left-aligned (column 0) rather than right-aligned to the column limit, keeping macro definitions compact.
  • BreakBeforeTernaryOperators: false — the ternary ? / : stays at the end of the line, not the start of the continuation, matching the existing style.
# ── Miscellaneous ─────────────────────────────────────────────────────────────
MaxEmptyLinesToKeep: 2
FixNamespaceComments: false
SortUsingDeclarations: false
AlignEscapedNewlines: Left
BreakBeforeTernaryOperators: false

How to use

Format sources in-place

Reformats all .cpp, .hpp, and .h files under projects/ using the canonical .clang-format config. Run before opening a PR or after pulling changes that modify the config.

See How do I format C++ sources with clang-format? for the full recipe.

cmake --build --preset linux-clang-debug-make --target format

Check for drift (dry run)

Reports files that differ from the canonical style without modifying them. Exits non-zero if any file would change — use this locally to verify a branch is clean before pushing.

cmake --build --preset linux-clang-debug-make --target check-format

Nightly CI workflow

The nightly-format GitHub Actions workflow runs at 02:00 UTC every night. It installs clang-format-18 on Ubuntu 24.04, reformats all sources in-place, and — if any file changed — raises a PR titled [format] Nightly clang-format run against main. When the PR is empty (no files changed) no action is taken.

To trigger it manually: Actions → Nightly Format → Run workflow.

Updating the style

To change a setting:

  1. Edit the relevant #+begin_src yaml block in this document.
  2. Update the documentation prose and example above the block.
  3. Run compass build --direct tangle_clang_format to regenerate .clang-format.
  4. Run check-format to see what changes; review the diff.
  5. Commit both this document and the regenerated .clang-format.

See also

Emacs 29.1 (Org mode 9.6.6)