CMake Unity Builds: Find Translation-Unit Assumptions Before Enabling Faster Compilation
Enable unity builds only after comparing their semantics with ordinary separate compilation. Combining source files can turn independent internal names into collisions and allow a preprocessor definition in one file to affect another. A successful combined compile can therefore be more concerning than a clear redefinition error.
This guide pins CMake 3.31.12 documentation and uses GCC 14.2.0 host fixtures. CMake was not installed: the executed tests manually included source files into a common translation unit to reproduce the mechanism. The CMake configurations below are proposed, and no compilation-speed improvement was measured.
Understand what the build transformation changes
The CMake unity-build reference describes generated source files that include several original sources. The compiler processes each generated file as one translation unit. This changes the boundary within which internal linkage, anonymous namespaces, macros and include guards operate.
The default mode is BATCH, with a configurable upper bound on sources per unity file. GROUP mode uses explicit UNITY_GROUP assignments; ungrouped sources compile individually. CMake keeps different source languages in separate unity files rather than mixing C and C++ into one unit.
Preserved source order does not make order-dependent source code desirable. A change in grouping, batch size or target source list can expose a hidden assumption that happened to remain harmless under an earlier generated layout.

Use an internal-name collision as the first control
The executed C fixture had two files, each defining a private static integer named state. One file initialized it to one, the other to two; each exposed a differently named accessor. Separate compilation and linking passed the test requiring the two distinct values.
A generated-style wrapper then included both files:
/* unity.c: executed compiler-level fixture */
#include "a.c"
#include "b.c"
Compiling the wrapper failed with a redefinition diagnostic. The static declarations no longer occupied separate translation units. This is an expected failure control, not evidence that static itself is unsuitable for normal modular C code.
In C++, anonymous namespaces can create the analogous collision when source files are combined. Review helper names and file-local types as well as variables. A private name should not be renamed throughout a project without first establishing which source grouping caused the conflict.
Look for silent macro leakage
A second fixture made the problem observable without a compiler error. The first source defined BIAS as nine and returned it. The second defined BIAS as two only if it was not already defined, then returned that value.
| Executed host case | Accessor results | Interpretation |
|---|---|---|
| Separate source compilation | 9 and 2 | Each translation unit had its own macro history. |
| Combined inclusion | 9 and 9 | The first file’s definition changed the second file. |
| Explicit undef between includes | 9 and 2 | A controlled cleanup restored this fixture’s intended context. |
| Sources kept separate | 9 and 2 | The manual exclusion equivalent preserved ordinary behavior. |
The combined program compiled and ran successfully while returning the wrong second value. A build-only gate would miss that failure. Keep the same functional assertions in normal and unity configurations instead of accepting a different result merely because the build mode changed.
The explicit undef was a narrow diagnostic control. It does not justify a generic cleanup hook that guesses every macro a third-party file might define. Include guards, packing pragmas and compiler-specific state can require different handling.
Choose a local remedy
For a known problematic file, the source exclusion property allows a precise proposed remedy:
set_source_files_properties(legacy_parser.c PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON)
Set the property in the appropriate CMake directory scope and inspect the generated build to confirm the file compiles independently. The article’s manual exclusion fixture did not execute this CMake command; it only established the underlying separate-compilation behavior.
For reviewed C++ code, UNITY_BUILD_UNIQUE_ID supplies a per-file identifier that can help disambiguate nested names. It does not automatically rewrite existing names or isolate all preprocessor state. The code must deliberately use the identifier, including a fallback for non-unity builds.
Alternatively, repair a source’s accidental macro leak when the intended local contract is clear. Prefer explicit ownership and cleanup in that source over relying on its current neighbor in a generated wrapper. Keep genuinely incompatible files outside unity builds.
Inspect automatic exclusions before claiming coverage
In the pinned CMake release, a source with nonempty source-level COMPILE_OPTIONS, COMPILE_DEFINITIONS, COMPILE_FLAGS or INCLUDE_DIRECTORIES is excluded from unity inclusion. Relevant C++ module scanning also excludes sources. A successful unity configuration may therefore leave some files separately compiled.
Record the actual generated grouping and the commands used for every critical file. Do not label the whole target “unity tested” solely because the target property is enabled. The source that carries the risky assumption may never have entered a combined unit.
Keep a paired acceptance build
Use separate clean build directories for ordinary and unity configurations with the same compiler, target, definitions and functional test suite. Inspect differences in generated grouping before comparing outcomes. Add a small number of deliberately different batch/group arrangements when order sensitivity is suspected.
Keep unity enablement under developer control, consistent with the CMake guidance. A target known to be incompatible can explicitly disable it. Any claimed build-time benefit needs measurements of the actual project and machine; these tiny fixtures provide no such evidence.
Acceptance requires matching functional behavior, understood exclusions and repeatable grouping-related controls. The host results demonstrate two concrete failure modes and local remedies. Target behavior, CMake generation and a real project’s performance still need their own qualification.