11 KiB
Tooling for the C/C++ Coding Style Guide
This repo ships five files that automate as much of cpp-style-guide.md as tooling can reach:
| File | Enforces |
|---|---|
.clang-format |
All formatting: tabs, Allman braces, namespace indentation, pointer alignment, include order, line length |
.clang-tidy |
Naming conventions for Part I (C++), plus a subset of the Language Feature Do's and Don'ts |
c-api/.clang-tidy |
Naming conventions for Part II (C API) — snake_case instead of camelCase/PascalCase |
.editorconfig |
Baseline tabs/whitespace for editors that don't run clang-format live |
.gitattributes |
Forces LF line endings for source files on every OS, so diffs and formatting checks agree across Linux/Windows/Mac |
.githooks/pre-commit |
Runs clang-format + clang-tidy automatically on every commit — pure bash, no Python or third-party framework, works on Linux/macOS/Windows |
Put all of these at your repo root except c-api/.clang-tidy, which goes at the root of whatever directory holds your Part II surface (rename the folder to match your actual layout — c-api/, public-include/, etc.).
.clang-format
Covers §2 Indentation, §3 Brace Placement, §4 Pointers and References, §5 Spacing, §7 Include Directive Order, §8 Line Length, and Namespaces (indentation + no trailing brace comments), in full — running clang-format -i reformats a file to match all of these automatically. Also partially supports Class Member Organization: AllowShortFunctionsOnASingleLine: InlineOnly lets the trivial-one-liner exception stay a compact one-liner in the class body while still fully Allman-expanding anything defined out-of-class — it doesn't enforce the rest of that section (member order, where an inline template member sits within its section).
One caveat: the include-order regexes classify a header by how it's written, not what it actually is — <vector> is correctly detected as C++ standard library, but a third-party header with no slash and no extension (rare, but it happens) could be miscategorized. Spot-check unusual includes rather than trusting the grouping blindly.
.clang-tidy
Covers the naming table in §1 Naming Conventions for Part I via readability-identifier-naming, plus a handful of checks tied to specific rules in §10 Language Feature Do's and Don'ts:
cppcoreguidelines-owning-memory/cppcoreguidelines-no-malloc→ Memory and Ownership: flags raw owning pointers and C-style allocation.modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-cstyle-cast→ Modern C++ Features.modernize-concat-nested-namespaces→ Namespaces: rewrites manually nestednamespace network_utils { namespace detail { ... } }into the requirednamespace network_utils::detail { ... }form (clang-tidy --fixapplies it automatically).bugprone-exception-escape→ Error Handling: flags exceptions escapingnoexceptcontexts, including destructors.GlobalVariableCase/GlobalVariablePrefixandStaticVariableCase/StaticVariablePrefix→ §1:g_/s_for mutable global andstaticvariables.ClassMemberCase/ClassMemberPrefixcovers the sames_rule for static class data members specifically, kept separate from them_-prefixed instance members. Constants are unaffected —GlobalConstantCasestill requiresPascalCasewith no prefix, since aconst/constexprglobal isn't the mutable state these prefixes exist to flag.readability-redundant-inline-specifier→ Class Member Organization: flags an explicitinlinekeyword that's already redundant (e.g. on a function defined in the class body). A narrow, complementary check — it doesn't catch an inline body that omits the keyword, which is the common case that rule is actually about.
c-api/.clang-tidy
A second, directory-scoped .clang-tidy that overrides naming to match §13 Naming Conventions (C API) — snake_case functions/variables, UPPER_CASE enum values and macros — without touching the rest of the project. clang-tidy always walks up from the file being checked to the nearest .clang-tidy, so this is the standard way to give one subtree different rules; no per-file flags needed. InheritParentConfig: true keeps the shared checks (like bugprone-exception-escape) from the root config active here too.
.editorconfig
A fallback for editors/IDEs that display tabs-vs-spaces before clang-format has a chance to run on save. Doesn't replace .clang-format — just prevents a raw, unformatted first draft from using spaces.
.gitattributes
Forces LF line endings for source files on checkout, regardless of each contributor's OS or local core.autocrlf setting. Without this, the exact same file can end up CRLF on a Windows checkout and LF on Linux/Mac — which makes clang-format's output and the on-disk file disagree on every single line, not because of real formatting drift but purely because of line endings. This is what makes the .githooks/pre-commit diff check (and clang-format in general) behave identically across platforms.
.githooks/pre-commit
A plain bash script — no Python, no pip install, no third-party framework — that runs clang-format and clang-tidy against staged C/C++ files before each commit.
Setup (once per clone):
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
Tool availability is optional, by design:
- If
clang-formatorclang-tidyisn't found onPATH, the hook prints a yellow warning and skips that check — it never blocks a commit just because a contributor hasn't installed the tools. - If a tool is installed and finds real formatting or lint issues, the commit is blocked with a red error and the fix command (
clang-format -i ...). clang-tidyadditionally needscompile_commands.jsonto resolve#includes; if that's missing, it's treated the same as a missing binary — a warning, not a failure. Generate one withcmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON(orbear -- makefor non-CMake builds).
Because core.hooksPath is a local git config, each contributor runs the setup command once — it isn't automatically enabled by cloning the repo. If you want it enforced repo-wide regardless of local setup, run the same checks in CI as a second, unconditional layer.
Platform notes
The whole toolset — .clang-format, .clang-tidy, c-api/.clang-tidy, .editorconfig, .gitattributes, and .githooks/pre-commit — works unmodified on Linux, macOS, and Windows. A few specifics:
- Linux: install the tools via your distro's package manager, e.g.
apt install clang-format clang-tidy(Debian/Ubuntu) or the equivalent for your distro. - macOS:
brew install clang-format llvm(Homebrew'sllvmformula includesclang-tidy; it isn't symlinked ontoPATHby default —brew --prefix llvmshows where it installed, add itsbin/toPATH). - Windows: install LLVM from releases.llvm.org or
winget install LLVM.LLVM, which putsclang-format.exe/clang-tidy.exeonPATH. The.githooks/pre-commitscript itself needs no separate installation or WSL — it runs through Git for Windows' bundled Git Bash automatically, sincegit.exereads the#!/usr/bin/env bashline at the top of the hook and executes it through that shell. If a contributor doesn't have the LLVM tools installed, the hook warns and skips, exactly as on Linux/Mac. - Line endings:
.gitattributesnormalizes everything to LF on checkout regardless of platform or a contributor'score.autocrlfsetting, soclang-format's output always matches the on-disk file byte-for-byte and the hook's diff check doesn't produce false positives from CRLF/LF mismatches.
What these tools can't check
Some rules in the guide are structural or naming-adjacent in ways clang-format/clang-tidy can't reach. These stay as code-review checklist items:
- File naming — §1's
PascalCase.hpp/.cppfile names and §13'ssnake_case.h/.cfile names. Neither tool renames or checks file names. - One type per header — §6 and the one-template-per-header rule in Templates. No standard clang-tidy check counts top-level declarations per file.
- The
#include/forward-declare choice and the Pimpl idiom — Headers as Public API is a design decision, not a formatting or naming rule. - The C API symbol-prefix convention (e.g.
net_manager_*) in §13 —c-api/.clang-tidyenforces the casing, but a project-specific prefix isn't something a generic naming check can require without hardcoding your library name into the config. - Exceptions-for-true-exceptions-only in Error Handling — whether a given
throwrepresents a genuinely exceptional condition versus expected control flow is a judgment call no linter makes reliably. - The static/dynamic/runtime-load guidance in §11 — these are architectural decisions about what crosses a library boundary, not something a formatter enforces per line.
- Doxygen comment syntax over plain
//in §9 —clang-formatdoesn't rewrite comment styles, and no standardclang-tidycheck flags a plain//comment sitting above a declaration. Doxygen itself (this project'sscripts/docs.sh) will happily generate a page for an undocumented symbol; it doesn't know or care what style a comment above it uses. - Blank line after an early exit (
return/break/continue) in §12 — neither tool has a check tied to specific statement kinds like this. - Member order (
public/protected/private) and inline-template-member placement in Class Member Organization — no standard check inspects access-specifier ordering or where a member is positioned within a section. - Avoiding inline member function definitions in Class Member Organization —
readability-redundant-inline-specifier(.clang-tidy) catches one narrow related mistake, a redundant explicitinlinekeyword, but not an inline body that omits the keyword, which is the common case.