Read this when you write C++ for a ROS 2 package or a robot SDK (Unitree Go2, Dynamixel), or C for firmware, and want your editor, your reviewer and CI to agree on style.
We follow the ROS 2 code style everywhere, including code that never touches ROS, so one .clang-format and one .clang-tidy serve every lab repo. The one thing to remember: format with clang-format, and in ROS 2 packages run ament_clang_format instead of ament_uncrustify, because the two formatters cannot both be satisfied.
- The style we follow
- Language versions
- Naming and file layout
- Install the tools
- Editor setup
- Adopt the config in a repo
- Check in CI
- Common mistakes
- Related
The ROS 2 code style builds on the Google C++ Style Guide with a few changes. We use it because most of our C++ lives in ROS 2 packages, and matching the ecosystem means code moves between lab repos and upstream projects without a reformat.
The changes from Google style that you will notice:
| Rule | ROS 2 style |
|---|---|
| Line length | 100 characters |
| Braces | Open brace on its own line for function, class, struct and enum definitions; on the same line for if, else, for, while |
| Always braces | Even for one-line if and loop bodies |
| Pointers and references | char * c; and const Limits & limits (space on both sides) |
| Access specifiers | public: in the same column as class |
| Function names | snake_case is allowed and is what ROS 2 core packages use; we use it |
| Exceptions | Allowed; avoid them in APIs you plan to wrap in C |
| Boost | Avoid unless there is no alternative |
For C, ROS 2 follows PEP 7 (Python's C style) with // comments allowed. Our .clang-format formats C files with the same settings.
configs/c-cpp/.clang-format is the configuration that ROS 2 ships in ament_clang_format, unchanged. We checked that clang-format 14 (Ubuntu 22.04), 18 (Ubuntu 24.04), 21 (Ubuntu 26.04) and 23 (the version Homebrew installs) all format our examples the same way; CI pins version 18.
configs/c-cpp/.clang-tidy turns on four check families and then removes the checks that are mostly noise in robot code:
| Family | What it catches | Turned off, and why |
|---|---|---|
bugprone-* |
Likely bugs: dangling handles, bad memset, suspicious string compares |
easily-swappable-parameters (fires on every function with two doubles), narrowing-conversions (every float/int mix in sensor code), reserved-identifier (ROS 2 header guards contain __) |
clang-analyzer-* |
Null dereferences, memory leaks, uses of uninitialized values (it follows each path through the code) | None |
performance-* |
Needless copies, std::move misuse |
avoid-endl |
modernize-* |
nullptr, using, range for, override |
avoid-bind (the ROS 2 style allows std::bind), avoid-c-arrays (vendor SDKs), use-nodiscard, use-trailing-return-type |
readability-* (16 picked checks) |
Missing braces, size() == 0, naming, redundant code |
Everything else in the family |
readability-identifier-naming enforces the naming table below. Every enabled check is an error (WarningsAsErrors: '*'), so keep the list short enough that this stays true.
The C and C++ standards depend on the ROS 2 distribution. Set them in CMakeLists.txt so every compiler agrees.
| ROS 2 distribution | C++ | C | Source |
|---|---|---|---|
| Humble, Jazzy, Kilted | C++17 | C99 | Jazzy code style page |
| Lyrical, Rolling | C++20 | C17 | Rolling code style page |
Code that must build on more than one distribution targets the oldest one, so C++17 for anything that still runs on Humble or Jazzy. Firmware uses the standard its board's toolchain supports; check the platform's docs.
| Thing | Style | Example |
|---|---|---|
| Namespaces, packages | snake_case |
teleop_velocity_limiter |
| Classes, structs, enums, type aliases | CamelCase |
VelocityLimiter |
| Functions and methods | snake_case |
limit(), on_command() |
| Variables and parameters | snake_case |
dt_seconds |
| Private and protected members | snake_case_ with a trailing underscore |
last_command_ |
| Global variables | g_ prefix |
g_shutdown_requested |
| Macros | UPPER_CASE |
RING_BUFFER_CAPACITY |
| Constants and enum values | Any consistent style in the package (ROS 2 mixes kName, NAME and name) |
kMaxSpeed |
| Header guards | PACKAGE__PATH__FILE_HPP_ (what ament_cpplint expects) |
TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_HPP_ |
| Files | snake_case.hpp and snake_case.cpp; C uses .h and .c |
velocity_limiter_node.cpp |
A ROS 2 C++ package keeps public headers in include/<package>/, sources in src/ and tests in test/; launch and parameter files go in launch/ and config/. ROS 2 packages has the full layout; examples/c-cpp/teleop_velocity_limiter/ is a working example.
macOS (Homebrew):
brew install clang-format
brew install llvm # provides clang-tidy; Homebrew does not put it on PATH
echo 'export PATH="$(brew --prefix llvm)/bin:$PATH"' >> ~/.zshrcUbuntu 24.04 (clang-format and clang-tidy 18) or 22.04 (version 14):
sudo apt update
sudo apt install clang-format clang-tidyFor ROS 2 packages, also install the ament wrapper (replace jazzy with your distribution):
sudo apt install ros-jazzy-ament-cmake-clang-formatVS Code has two good options. Pick one; running both gives duplicate diagnostics.
-
C/C++ extension (
ms-vscode.cpptools, Microsoft): formats with clang-format and reads.clang-formatby default (C_Cpp.clang_format_styleisfile). Turn on clang-tidy with"C_Cpp.codeAnalysis.clangTidy.enabled": true. -
clangd extension (
llvm-vs-code-extensions.vscode-clangd): reads.clang-formatand merges your.clang-tidyinto its live diagnostics. It looks forcompile_commands.jsonin parent folders and inbuild/; in a colcon workspace, point it at the package's database with a.clangdfile:CompileFlags: CompilationDatabase: build/teleop_velocity_limiter
Both need a compilation database for accurate results. In a colcon workspace:
colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ONTurn on format on save in .vscode/settings.json:
{
"[cpp]": { "editor.formatOnSave": true },
"[c]": { "editor.formatOnSave": true }
}Run this from the repo root (a single package, a colcon workspace repo or a firmware project):
BASE=https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs
curl -fsSLO "$BASE/c-cpp/.clang-format"
curl -fsSLO "$BASE/c-cpp/.clang-tidy"
# Reformat everything once, in its own commit, so later diffs stay small
find . -path ./build -prune -o \( -name '*.[ch]pp' -o -name '*.[ch]' \) -print0 | xargs -0 clang-format -i
git add .clang-format .clang-tidy
git commit -am "Format C++ with the lab clang-format config"In each ROS 2 package that uses ament_lint_auto, swap uncrustify for clang-format. Add the test dependency to package.xml:
<test_depend>ament_cmake_clang_format</test_depend>Then exclude uncrustify in CMakeLists.txt, before ament_lint_auto_find_test_dependencies():
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# We format with clang-format; uncrustify disagrees with it on lambdas and one-line functions.
list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify)
ament_lint_auto_find_test_dependencies()
endif()ament_clang_format uses the same rules as our file, so it needs no extra settings.
This GitHub Actions job builds and tests a ROS 2 repo on Jazzy and runs clang-format and clang-tidy. We ran these steps in a ros:jazzy-ros-base container against examples/c-cpp/ before publishing them; rosdep installs ament_cmake_clang_format from your package.xml.
name: cpp
on: [push, pull_request]
jobs:
lint-build-test:
runs-on: ubuntu-latest
container: ros:jazzy-ros-base
steps:
- uses: actions/checkout@v7
- name: Install tools and dependencies
run: |
apt-get update
apt-get install -y clang-format clang-tidy jq
rosdep update
rosdep install --from-paths . --ignore-src -y
- name: clang-format
run: find . \( -name '*.[ch]pp' -o -name '*.[ch]' \) -print0 | xargs -0 clang-format --dry-run --Werror
- name: Build, clang-tidy and test
shell: bash
run: |
source /opt/ros/jazzy/setup.bash
colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# clang-tidy on our own sources only (not generated files or system libraries)
for db in build/*/compile_commands.json; do
files=$(jq -r '.[].file' "$db" | grep "^$PWD/" | grep -v "^$PWD/build/" | sort -u || true)
if [ -n "$files" ]; then clang-tidy -p "$(dirname "$db")" --quiet $files; fi
done
colcon test
colcon test-result --verbose- Running two formatters.
ament_uncrustify --reformatand clang-format undo each other's changes. Pick clang-format and exclude uncrustify, as above. - clang-tidy without a compilation database. It then guesses include paths and reports missing headers. Build with
-DCMAKE_EXPORT_COMPILE_COMMANDS=ONand pass-p build/<package>. #pragma oncein ROS 2 packages.ament_cpplintexpects#ifndefguards named after the path.- Formatting vendor code. Keep vendor SDKs (Unitree, Dynamixel) in their own folder and exclude it from
find, so updates stay easy to diff.HeaderFilterRegexalready keeps clang-tidy out of headers outsideinclude/andsrc/. - A reformat mixed into a feature commit. Reformat in a separate commit so reviewers can see the real change.
- Committing
build/,install/orlog/. Add them to.gitignore(see Git and GitHub).
- ROS 2 packages: layout, naming and
ament_lint_auto - Hardware and firmware: PlatformIO and Arduino projects
- Python: the other half of most ROS 2 workspaces
- Config files:
configs/c-cpp/and the example package inexamples/c-cpp/ - All things ROS 2: ROS 2 basics, distributions and installation
- clang-format style options and clang-tidy check list
Last reviewed: Sept. 30, 2026 · Sources checked on that date · Suggest a change