# C and C++ modernization with clang-tidy and Droids

> C++ modernization starts with clang-tidy modernize checks and a compilation database. Coccinelle handles C API changes, c2rust produces a first unsafe Rust pass, and Droids fix the residue while compilers, sanitizers, and tests decide what ships.

URL: https://factory.com/articles/cpp-modernization-clang-tidy
Published: 2026-11-14
Updated: 2026-11-14

C++ modernization rests on mature tooling. Clang parses the code exactly as the compiler does, and clang-tidy turns that parse into checks with automatic fixes. A team can move a large codebase from C++98 idioms to modern C++ with a series of mechanical, reviewable passes. Factory adds Droids to that process for the work the fixes cannot do, and keeps the compiler, sanitizers, and tests as the judges.

C has a different toolset. Coccinelle applies semantic patches for API changes across C code, and c2rust translates C to Rust as a deterministic first pass. Both follow the same split described in [deterministic code modernization with Factory](https://factory.com/articles/deterministic-code-modernization). The tool does the repeatable bulk, and the Droid handles the residue.

## Start from a compilation database

Every Clang-based tool needs to know how each file is compiled. A [JSON compilation database](https://clang.llvm.org/docs/JSONCompilationDatabase.html), conventionally named `compile_commands.json`, records the compiler arguments, working directory, and source file for each translation unit. CMake writes one when the build is configured with `CMAKE_EXPORT_COMPILE_COMMANDS`. Projects built with plain Make can use Bear to intercept compiler calls, and Meson writes one automatically.

Without that file, include paths and preprocessor defines are guesses, and a fix that compiles in the tool can break in the real build. Generate the database from the same configuration CI uses, and regenerate it whenever build flags change.

## Mechanical C++ modernization with clang-tidy

[clang-tidy](https://clang.llvm.org/extra/clang-tidy/) groups its checks by prefix. The `modernize-` group advocates modern language constructs, and most of its checks offer fix-its that `-fix` applies. A few examples show the range.

- `modernize-use-nullptr` replaces `NULL` and literal `0` null pointer constants with `nullptr`.
- `modernize-use-override` adds `override` to overriding member functions and removes redundant `virtual`.
- `modernize-loop-convert` rewrites index and iterator loops as range-based `for` loops when it can show the conversion is safe.
- `modernize-make-unique` replaces `std::unique_ptr(new T(...))` with `std::make_unique(...)`.

For a whole project, `run-clang-tidy` runs clang-tidy over every entry in the compilation database in parallel. With `-fix`, it collects the fixes and applies them once, which avoids conflicting edits to shared headers. Run one check per pass and commit each pass alone. A reviewer can then confirm a ten-thousand-line diff by reading the check's documentation and regenerating it. Follow each pass with `clang-format` so formatting noise never mixes with agent edits.

Commit a `.clang-tidy` file that lists the enabled checks, so editors, CI, and Droid hooks all run the same configuration. Use `-header-filter` to limit diagnostics and fixes to the project's own headers, which leaves vendored and third-party code alone.

The fixes stop at semantic decisions.

`modernize-make-unique` only rewrites code that already uses `std::unique_ptr`. Deciding that a raw owning pointer should become a `unique_ptr`, or that a parameter should take a reference, means reading the call sites and the ownership rules of the code. Some checks, such as `modernize-avoid-c-arrays`, report findings with no automatic fix at all. Fixes inside macros, conditionally compiled code, and generated headers can produce a build that works in one configuration and breaks in another. A Droid takes these one at a time, reads the surrounding code and the check documentation, makes the change, and lets the build decide.

Public headers of a shared library need extra care. Adding `override` does not change the binary interface, but `modernize-pass-by-value` changes constructor signatures, and that breaks callers compiled against the old header. A skill can tell the Droid to flag any change to an exported signature for a person to approve instead of applying it.

When a pattern repeats across hundreds of files and no check covers it, the Droid writes one. clang-tidy supports custom checks, and a check with a fixture test becomes a deterministic, rerunnable pass like the built-in ones.

## Semantic patches for C with Coccinelle

[Coccinelle](https://coccinelle.gitlabpages.inria.fr/website/) is a matching and transformation engine for C. Its Semantic Patch Language describes a change in terms of code structure, so one patch applies wherever the pattern appears regardless of formatting or variable names. It was built for collateral evolutions in Linux, where a library API change requires updates in every caller, and the kernel ships semantic patches in its source tree.

This semantic patch moves every call of an old logging function to a new signature.

```text
@@
expression ctx, msg;
@@
- log_msg(ctx, msg)
+ log_event(ctx, LOG_INFO, msg)
```

Running `spatch --sp-file log.cocci --dir src` prints the resulting patch, and adding `--in-place` writes the changes to the files. A Droid is effective at writing these patches. It drafts the patch, runs it on a small fixture file with known before and after versions, and refines the patch until the output matches. The finished `.cocci` file then runs across the codebase deterministically and can be kept for the next release.

## Gates for C and C++ changes

Compilation is the first gate, with warnings treated as errors for files touched by the migration. It is not sufficient for C and C++, where a change can compile cleanly and still introduce undefined behavior.

Sanitizers close part of that gap. [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html), enabled with `-fsanitize=address`, detects out-of-bounds accesses, use-after-free, double-free, and related memory errors, with a typical slowdown of about 2x according to its documentation. UndefinedBehaviorSanitizer, enabled with `-fsanitize=undefined`, catches a separate class of errors such as signed overflow. Run the test suite under both in CI for every modernization pass, because a loop conversion or ownership change that alters object lifetime shows up here before it shows up in production.

The last gate reruns the tools without changing anything. Run `run-clang-tidy` with the same checks and no `-fix`, and require zero findings for the migrated checks. Run `clang-format --dry-run --Werror` and require a clean exit. Together these show that the Droid's edits did not reintroduce a pattern a pass removed.

## C to Rust with c2rust as the first pass

[c2rust](https://github.com/immunant/c2rust) translates C99 code to Rust. The project states that the transpiler produces unsafe Rust that closely mirrors the C input and preserves functionality, and that this output is unidiomatic. Its stated goal is that test suites keep passing after translation. Like clang-tidy, `c2rust transpile` reads `compile_commands.json`, and it emits a warning and skips function definitions it cannot translate.

Getting from that output to safe, idiomatic Rust is the real migration. The c2rust project provides a deterministic refactoring tool for cleanup and an LLM-powered postprocessor, and it recommends using the postprocessor with a robust test suite because it can introduce errors.

The same caution applies to any agent in this step. A Droid works module by module, replacing raw pointers with references and slices, swapping libc calls for the standard library, and shrinking `unsafe` blocks behind safe interfaces. After each change, `cargo build`, `cargo test`, and `cargo clippy` run, along with the original C test suite or differential tests that compare the Rust and C builds on the same inputs. A module counts as done when it passes those gates with no new `unsafe` code.

## How Factory runs the C and C++ loop

Factory runs this loop with the build system and sanitizers as its gates.

Deterministic pass</span><br/><span style='color:#8A8380 !important'>run-clang-tidy -fix, spatch, c2rust</span>"]
    B["<span style='color:#FAFAFA !important;font-weight:600'>Build and test gate</span><br/><span style='color:#8A8380 !important'>-Werror, ASan, UBSan, test suite</span>"]
    C{"<span style='color:#FAFAFA !important;font-weight:600'>Gate passes?</span>"}
    D["<span style='color:#FAFAFA !important;font-weight:600'>Droid handles residue</span><br/><span style='color:#8A8380 !important'>build breaks, ownership, no fix-it</span>"]
    E["<span style='color:#FAFAFA !important;font-weight:600'>Deterministic re-run</span><br/><span style='color:#8A8380 !important'>clang-tidy without -fix, zero findings</span>"]
    F["<span style='color:#FAFAFA !important;font-weight:600'>Human review</span><br/><span style='color:#8A8380 !important'>tool commit and agent commit</span>"]
    A --> B --> C
    C -- no --> D --> B
    C -- yes --> E --> F
`}
/>

**Check every edit.** A `PostToolUse` [hook](https://docs.factory.com/harness/hooks) runs after each file write. This script formats the edited source file, runs clang-tidy against the build's compilation database, and exits with code 2 on failure so that the diagnostics go back to the Droid.

```bash
#!/usr/bin/env bash
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.c|*.cc|*.cpp) ;;
  *) exit 0 ;;
esac
clang-format -i "$file"
if ! out=$(clang-tidy -p "$FACTORY_PROJECT_DIR/build" --quiet --warnings-as-errors='modernize-*' "$file" 2>&1); then
  echo "$out" >&2
  exit 2
fi
```

**Keep the playbook in the repository.** A [skill](https://docs.factory.com/harness/skills) records which checks run in which order, the sanitizer build command, and the rule that each tool pass is committed alone before any agent edits.

**Fan out over findings.** Factory's [lint autofix recipe](https://docs.factory.com/droid-exec/recipes/lint-autofix) shows the pattern with ESLint, and it carries over to clang-tidy. The tool lists files with findings that have no fix-it, a script runs one `droid exec --auto low` per file, and clang-tidy reruns on each file to confirm the finding is gone.

**Plan large migrations.** For a migration across many libraries, or a module-by-module move from C to Rust, [Missions](https://docs.factory.com/missions/overview) plan the work as milestones and validate each one before the next begins.

These commands cover the deterministic passes in this loop.

<div className="not-prose my-6 flex flex-col gap-3 [&_button]:!w-auto">
  <div className="grid grid-cols-1 gap-y-5 md:grid-cols-[max-content_1fr] md:gap-x-4">
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Compilation database.</strong> Writes build/compile_commands.json for Clang tools and c2rust.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">One modernize pass.</strong> Applies a single check across the project in parallel.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">C API change.</strong> Applies a Coccinelle semantic patch to every C file under src.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">C to Rust first pass.</strong> Produces unsafe Rust that mirrors the C code.</span>
  </div>
</div>

## Frequently asked questions

### Which clang-tidy checks are used for C++ modernization?

The modernize- group, which the clang-tidy documentation describes as checks that advocate modern language constructs. Examples include modernize-use-nullptr, modernize-use-override, modernize-loop-convert, and modernize-make-unique. Most provide automatic fixes, and some, such as modernize-avoid-c-arrays, only report.

### Why does clang-tidy need compile_commands.json?

clang-tidy parses each file as a compiler would, so it needs the exact flags, include paths, and defines used in the build. A JSON compilation database records them. CMake writes one when CMAKE_EXPORT_COMPILE_COMMANDS is enabled, and Bear can intercept other builds.

### Does c2rust produce safe Rust?

No. The c2rust project states that c2rust transpile produces unsafe Rust that closely mirrors the C input and is unidiomatic. It is a first step that preserves behavior, and turning it into safe, idiomatic Rust takes additional work.

### What does the Droid do in a clang-tidy migration?

It fixes what the automatic fixes leave, such as build breaks from macros or headers, findings with no fix-it, ownership decisions when replacing raw pointers, and failing tests. It can also write new Coccinelle semantic patches or clang-tidy checks with test fixtures.

## Further reading

- [How to run a runtime upgrade with coding agents](https://factory.com/articles/coding-agent-runtime-upgrades)
- [Enterprise software modernization beyond the demo](https://factory.com/articles/enterprise-software-modernization)
- [Droid Exec overview](https://docs.factory.com/droid-exec/overview)
- [Missions planning and validation](https://docs.factory.com/missions/planning)
