Modernization
Software Factory
C and C++ modernization with clang-tidy and Droids
November 14, 2026 - 6 minute read
Modernization
Software Factory
November 14, 2026 - 6 minute read
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. The tool does the repeatable bulk, and the Droid handles the residue.
Every Clang-based tool needs to know how each file is compiled. A JSON compilation database, 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.
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<T>(new T(...)) with std::make_unique<T>(...).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.
Coccinelle 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.
@@
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.
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, 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.
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.
Factory runs this loop with the build system and sanitizers as its gates.
Check every edit. A PostToolUse hook 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.
#!/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
fiKeep the playbook in the repository. A skill 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 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 plan the work as milestones and validate each one before the next begins.
These commands cover the deterministic passes in this loop.
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.
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.
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.
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.
Plan legacy modernization with private AI around behavior preservation, internal dependencies, and reviewable milestones. Keep code and validation inside the approved boundary.
A Ruby and Rails upgrade goes one minor version at a time. RuboCop, bin/rails app:update, dual boot, and Synvert handle the mechanical changes, deprecation warnings become the work list, and Factory Droids fix the residue under the same deterministic checks.
PHP modernization works best when Rector performs the mechanical upgrade, PHPStan and the test suite gate every step, and Factory Droids handle the residue and write new Rector rules. A practical setup with hooks, skills, and Droid Exec.
Start building