Build System Support for C++20 Modules
The C++20 module language features are clean and well-specified. The build-system story is messier — and it is the main practical reason module adoption lagged behind the standard itself. This lesson explains why, and shows you how to wire up modules correctly with CMake 3.28, Clang, MSVC, and GCC.
Every build system detail in this lesson is compiler-specific and tool-version-specific. The canonical reference for each toolchain is its own documentation — treat this page as the map, not the territory.
Why modules require a smarter build system
With header files, the preprocessor simply copies text. Every translation unit (.cpp file) is self-contained after the preprocessor runs, and build systems can compile them in any order or in parallel. There are no cross-file dependencies that must be resolved at compile time.
Modules change this fundamentally. When the compiler encounters import geometry;, it needs the pre-compiled binary module interface (BMI) for the geometry module to be on disk before it can proceed. The BMI is a compiler-specific binary that encodes the exported declarations of a module — it is the module equivalent of a precompiled header, but generated automatically and consumed automatically. This creates a hard dependency ordering requirement: the module interface unit must be compiled before anything that imports it.
Classical Makefiles cannot express this: the dependencies are not known until after scanning each source file (which itself requires knowing the module dependencies first — a chicken-and-egg problem). Solving it requires a two-phase build: scan all sources for module dependencies first, build a dependency graph, then compile in the right order. This is what CMake 3.28 and Ninja's dyndep mechanism provide.
| Property | Headers (#include) | Modules (import) |
|---|---|---|
| Compilation order | Any order — files are independent | Must compile MIU before consumers |
| Compiler artifact | One .o per .cpp | BMI + .o per module interface unit |
| Dependency discovery | Preprocessor (fast, stateless) | Compiler scan step (before compile) |
| Parallel builds | Unlimited parallelism | Parallelism limited by dependency graph |
BMI files: names and locations
Each toolchain uses a different name and format for its BMI. Understanding what gets generated helps when debugging build failures:
| Compiler | BMI format | Default location | Notes |
|---|---|---|---|
| Clang 16+ | .pcm | Explicit -o flag required | Pre-Compiled Module |
| GCC 11+ | .gcm | gcm.cache/ subdirectory | GCC C++ Module (auto-placed) |
| MSVC 2019+ | .ifc | Explicit /ifcOutput flag | Interface File Cache |
File extensions for module interface units are also compiler-specific. Clang and most Linux toolchains favour .cppm (C++ module). MSVC traditionally uses .ixx but also accepts .cppm. CMake 3.28 treats whatever extension you give it correctly as long as you declare the file in a FILE_SET CXX_MODULES block.
CMake 3.28 — the recommended approach
CMake 3.28 (released December 2023) is the first stable release with production-quality C++20 modules support. It introduces a two-phase compilation model: CMake first runs a per-file "scan" step that extracts the module name from each .cppm file, builds a dependency graph, and then feeds that graph to the build tool (Ninja) so it can serialize BMI compilations correctly while maximising parallelism everywhere else.
The CMake side of the API is a single new keyword: FILE_SET CXX_MODULES on target_sources(). Any source file listed inside that file set is treated as a module interface unit and gets the two-step BMI+object treatment automatically.
Project layout
myproject/ ├── CMakeLists.txt ├── src/ │ ├── geometry.cppm # module interface unit │ ├── geometry_math.cppm # module interface unit (partition) │ └── geometry.cpp # module implementation unit (optional) └── main.cpp # consumer
CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(myproject LANGUAGES CXX)
# ── library with module interface units ──────────────────────────
add_library(geometry)
target_compile_features(geometry PUBLIC cxx_std_20)
# FILE_SET CXX_MODULES marks these as module interface units.
# CMake will scan, compile to BMIs, and track dependencies automatically.
target_sources(geometry
PUBLIC
FILE_SET CXX_MODULES FILES
src/geometry.cppm
src/geometry_math.cppm
PRIVATE
src/geometry.cpp # module implementation unit (regular compilation)
)
# ── executable that imports geometry ─────────────────────────────
add_executable(app main.cpp)
target_link_libraries(app PRIVATE geometry)Configure and build
# Ninja is required — Makefiles have limited/no modules support in CMake 3.28 cmake -B build -G Ninja -DCMAKE_CXX_STANDARD=20 . cmake --build build
Generator requirement
C++20 modules in CMake 3.28 require the Ninja generator (version 1.11+ for dyndep support). The Makefile generators do not support dynamic dependency graphs. On Windows, Visual Studio 2022 17.x generators also have experimental support.
| FILE_SET type | What it marks | Compile treatment |
|---|---|---|
| CXX_MODULES | Module interface units (.cppm) | Scan → BMI → object file |
| HEADERS | Header files for install/IDE | Not compiled (declaration only) |
| (default) | Regular .cpp implementation files | Compiled to object file only |
Clang — manual compilation steps
Understanding how Clang compiles modules manually is valuable for debugging and for understanding what CMake is doing under the hood. Module compilation with Clang is a three-step process per module: pre-compile to a .pcm BMI, compile to an object file using the BMI, then compile consumers with a reference to the BMI.
# geometry.cppm:
# export module geometry;
# import std;
# export struct Point { double x, y; };
# export double distance(Point a, Point b);
# ── Step 1: pre-compile MIU → .pcm (the BMI) ─────────────────────
clang++ -std=c++20 \
--precompile geometry.cppm \
-o geometry.pcm
# ── Step 2: compile the MIU → .o (links the definitions) ─────────
clang++ -std=c++20 \
-fmodule-file=geometry=geometry.pcm \
-c geometry.cppm \
-o geometry.o
# ── Step 3: compile consumer → .o ────────────────────────────────
# main.cpp starts with: import geometry;
clang++ -std=c++20 \
-fmodule-file=geometry=geometry.pcm \
-c main.cpp \
-o main.o
# ── Step 4: link ──────────────────────────────────────────────────
clang++ geometry.o main.o -o myprogramThe -fmodule-file=name=path.pcm flag maps a module name to its pre-compiled interface file. You must supply this flag for every module the current file imports. CMake generates these flags automatically when you use FILE_SET CXX_MODULES.
For the C++23 import std; named module with Clang, you first need to build the standard library module: clang++ -std=c++23 --precompile -stdlib=libc++ /path/to/std.cppm -o std.pcm. The path to std.cppm varies by libc++ installation; check $(llvm-config --includedir)/c++/v1/std.cppm.
MSVC — manual compilation steps
MSVC uses the /interface flag to mark a file as a module interface unit and produces an .ifc file as the BMI. It also produces a regular .obj. MSVC's preferred extension for module interface units is .ixx, though .cppm is also accepted with the right project settings.
REM ── geometry.ixx (module interface unit) ────────────────────── REM Step 1: compile MIU → .obj + .ifc cl /std:c++latest /EHsc /interface /c geometry.ixx ^ /ifcOutput geometry.ifc ^ /Fo:geometry.obj REM Step 2: compile consumer (main.cpp: import geometry;) cl /std:c++latest /EHsc /c main.cpp ^ /reference geometry=geometry.ifc ^ /Fo:main.obj REM Step 3: link cl geometry.obj main.obj /Fe:myprogram.exe
Key MSVC flags:
| /interface | Marks the file as a module interface unit (generates .ifc) |
| /ifcOutput path.ifc | Specifies the output path for the .ifc BMI file |
| /reference name=path.ifc | Provides the named module's .ifc to the compiler |
| /std:c++latest | Enables C++20/23 features including modules |
| /scanDependencies | Emit a JSON dependency file for build system use |
MSVC has first-class support for import std; when using Visual Studio 2022 17.5+ with /std:c++latest. The standard library module is pre-built and available automatically — no extra steps required.
GCC — experimental status
GCC's module support requires the -fmodules-ts flag (the "TS" refers to the Modules Technical Specification, which predated C++20). As of GCC 14, support is still marked experimental — it handles many real-world cases but has known limitations, particularly around the import std; named module and some partition edge cases.
# GCC places .gcm files automatically in gcm.cache/ # The module name is extracted from the source file by the compiler. # Compile module interface unit g++ -std=c++20 -fmodules-ts -c geometry.cppm # → geometry.o + gcm.cache/geometry.gcm (automatic) # Compile consumer g++ -std=c++20 -fmodules-ts -c main.cpp # GCC finds geometry.gcm automatically from gcm.cache/ # Link g++ geometry.o main.o -o myprogram
GCC's automatic placement of BMIs in gcm.cache/ is convenient for single-directory projects but can create issues with parallel builds in multi-directory layouts. CMake 3.28 handles GCC modules via the same FILE_SET CXX_MODULES mechanism, though Ninja is still required and GCC module support in CMake is less mature than Clang or MSVC.
The std named module (C++23)
C++23 standardises two named modules: std (everything in the standard library, including C compatibility headers, but through the std namespace) and std.compat (same as std plus C functions available in the global namespace). The support status differs by toolchain.
// With import std, you get everything without individual headers:
import std;
int main()
{
std::vector<int> v {1, 2, 3, 4, 5};
std::println("sum: {}", std::accumulate(v.begin(), v.end(), 0));
}| Toolchain | import std; status | Notes |
|---|---|---|
| MSVC 2022 17.5+ | Supported | Use /std:c++latest; pre-built, no extra steps |
| Clang 17+ / libc++ | Supported | Requires -std=c++23 -stdlib=libc++; build std.pcm first |
| GCC 15 | Experimental | Available in GCC 15; earlier versions: not supported |
CMake 3.30+ has dedicated support for the import std; case via cmake_standard_library(). For MSVC with CMake 3.28/3.29, add target_compile_features(myapp PRIVATE cxx_std_23)and MSVC handles import std; automatically.
Common gotchas
Using Make instead of Ninja
CMake's Makefile generators do not support the dynamic dependency graph that modules require. Always use -G Ninja (or the Visual Studio generator for Windows). Attempting to use make will either fail immediately or produce incorrect incremental builds.
Mixing import and #include in the same file
Any #include directives in a module interface unit must go in the global module fragment (before the export module declaration). Placing #include after export module is ill-formed. This trips up developers converting legacy code: move all #include directives above module;.
Forgetting that import doesn't propagate
If module A imports module B, and you import A in main.cpp, B's names are reachable (you can use them via auto), but not visible (you cannot spell the type names). You must import B explicitly if you need to name its types. This is intentional and prevents implicit transitive dependencies.
Template definitions must stay in the interface file
Templates need their complete definition at instantiation time. You cannot put a template function body in a module implementation file (.cpp) and expect it to be available to consumers who import the interface. Keep template definitions in the .cppm interface file.
Stale BMIs after renaming or moving files
BMI files are keyed by module name, not file path. If you rename a module or move its source file without cleaning the build directory, the build system may pick up a stale BMI. Always run cmake --build build --clean-first after structural reorganisation.
CMake version must be 3.28 or higher
CMake 3.26 and 3.27 had experimental MODULE support behind a cmake_experimental_cxx_import_std() gate. CMake 3.28 is the first stable release. Using an older version will either silently treat .cppm files as regular .cpp, or fail with errors about unknown FILE_SET types.
Minimum version requirements
| Component | Minimum version | Notes |
|---|---|---|
| CMake | 3.28 | Stable modules support; FILE_SET CXX_MODULES |
| Ninja | 1.11 | dyndep support required for CMake modules |
| Clang | 16 | Full C++20 modules; 17 for import std (libc++) |
| GCC | 11+ | Experimental: -fmodules-ts; production use from GCC 15 |
| MSVC (cl.exe) | VS 2019 16.8 | Stable; import std from VS 2022 17.5 |
| C++ standard flag | c++20 | c++23 required for import std; |