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 toNonetoo, but we make it explicit. Before:namespace ores { class Foo {};indented by 2 (LLVM default indent) After: namespace body at column 0.AccessModifierOffset: -4—public:/private:/protected:appear at the class indentation level, not further indented. Before: (LLVM default,-2with 2-space indent) = public:= After:public:at column 0 relative to class.IndentCaseLabels: true—case X:is indented relative toswitch. Before:switch (x) { case 1: ...at same level asswitchAfter:case 1:indented 4 spaces insideswitch.IndentPPDirectives: AfterHash— preprocessor directives are indented after the#, keeping nested#if/#endifreadable. 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: false—usingdeclarations 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:
- Edit the relevant
#+begin_src yamlblock in this document. - Update the documentation prose and example above the block.
- Run
compass build --direct tangle_clang_formatto regenerate.clang-format. - Run
check-formatto see what changes; review the diff. - Commit both this document and the regenerated
.clang-format.
See also
- How do I format C++ sources with clang-format? — the format and check-format recipe.
- Developer Links — nightly-format CI workflow and other build dashboards.
- CMake setup — preset names and build directory layout.
- Claude Code Settings — the analogous literate source for
.claude/settings.json.