Skip to content
C++

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.

Headers vs modules: compilation dependency model
PropertyHeaders (#include)Modules (import)
Compilation orderAny order — files are independentMust compile MIU before consumers
Compiler artifactOne .o per .cppBMI + .o per module interface unit
Dependency discoveryPreprocessor (fast, stateless)Compiler scan step (before compile)
Parallel buildsUnlimited parallelismParallelism 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:

CompilerBMI formatDefault locationNotes
Clang 16+.pcmExplicit -o flag requiredPre-Compiled Module
GCC 11+.gcmgcm.cache/ subdirectoryGCC C++ Module (auto-placed)
MSVC 2019+.ifcExplicit /ifcOutput flagInterface 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+

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 types at a glance
FILE_SET typeWhat it marksCompile treatment
CXX_MODULESModule interface units (.cppm)Scan → BMI → object file
HEADERSHeader files for install/IDENot compiled (declaration only)
(default)Regular .cpp implementation filesCompiled to object file only

Clang — manual compilation steps

Clang 16+

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 myprogram

The -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

VS 2019 16.8+

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:

/interfaceMarks the file as a module interface unit (generates .ifc)
/ifcOutput path.ifcSpecifies the output path for the .ifc BMI file
/reference name=path.ifcProvides the named module's .ifc to the compiler
/std:c++latestEnables C++20/23 features including modules
/scanDependenciesEmit 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 11+ (experimental)

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

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));
}
Toolchainimport std; statusNotes
MSVC 2022 17.5+SupportedUse /std:c++latest; pre-built, no extra steps
Clang 17+ / libc++SupportedRequires -std=c++23 -stdlib=libc++; build std.pcm first
GCC 15ExperimentalAvailable 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

ComponentMinimum versionNotes
CMake3.28Stable modules support; FILE_SET CXX_MODULES
Ninja1.11dyndep support required for CMake modules
Clang16Full C++20 modules; 17 for import std (libc++)
GCC11+Experimental: -fmodules-ts; production use from GCC 15
MSVC (cl.exe)VS 2019 16.8Stable; import std from VS 2022 17.5
C++ standard flagc++20c++23 required for import std;
Sign in to track progress