C++ modules provide a way to organize code and share declarations without relying entirely on traditional header files. They can improve compile-time behavior, reduce repeated parsing, and make dependencies more explicit. However, introducing modules into an existing C++ project can also create build problems that are difficult to diagnose.
A project may compile correctly when built in one environment but fail in another. A module interface might not be discovered, a compiler might not support the required module features, or a build system might attempt to compile a module consumer before its imported module is available.
CMake supports C++ modules through its build-system dependency scanning and compiler integration. When configured correctly, it can determine which source files provide modules, which files import them, and how those dependencies should affect the build order.
This article explains how C++ module dependencies work in CMake, how to configure a small project, and how to investigate common build failures.
Understanding C++ Modules and Their Dependencies
Traditional C++ projects commonly use header files to share declarations between source files. A source file includes a header, and the preprocessor inserts the header's contents before compilation.
C++ modules use a different model. A module interface unit provides declarations that other translation units can import.
For example, a module interface might look like this:
// math_utils.cppm
export module math_utils;
export int add(int a, int b)
{
return a + b;
}
The declaration export module math_utils; identifies the module interface. The exported function can be used by another translation unit that imports the module.
A consumer might look like this:
// main.cpp
import math_utils;
#include <iostream>
int main()
{
std::cout << add(10, 20) << '\n';
return 0;
}
The consumer does not include math_utils.cppm as a traditional header. Instead, it imports the module.
The compiler may produce a compiled module interface artifact, often called a BMI or a compiler-specific equivalent. The exact format and location depend on the compiler. Other translation units need the appropriate artifact before they can compile successfully.
This creates a build dependency:
math_utils.cppm → compiled module interface → main.cpp
The build system must understand that relationship. Merely adding both source files to an executable does not mean every compiler and build configuration can determine their correct order without module support.
Checking Compiler and CMake Support
Before debugging individual dependency errors, verify that the compiler and CMake versions support the features used by the project.
C++ modules support has evolved across compiler releases. Different compilers may support different parts of the standard, and support can depend on the selected language standard and compiler version.
Check the installed CMake version:
cmake --version
Check the compiler version:
g++ --version
For Clang, use:
clang++ --version
For Microsoft Visual C++, use the Developer Command Prompt:
cl
Then check the compiler's documented C++ module support for the version installed on the machine.
CMake's module dependency scanning is available through its supported C++ module build integration. The relevant behavior depends on the CMake version, generator, compiler, and project configuration. A configuration that works with one combination may not work with another.
For a modern CMake project, start with a suitable minimum version and explicitly select the C++ standard:
cmake_minimum_required(VERSION 3.28)
project(ModuleExample LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
CMake 3.28 is a reasonable baseline for using the modern C++ module file-set workflow, but it does not guarantee that every compiler and generator combination supports every module feature. Check the compatibility requirements for your actual toolchain.
Creating a Minimal CMake Project
A small project makes it easier to separate module problems from unrelated application code.
Use the following directory structure:
ModuleExample/
├── CMakeLists.txt
├── main.cpp
└── math_utils.cppm
The module interface is:
// math_utils.cppm
export module math_utils;
export int add(int a, int b)
{
return a + b;
}
The application source is:
// main.cpp
import math_utils;
#include <iostream>
int main()
{
std::cout << "Result: " << add(10, 20) << '\n';
return 0;
}
Configure the module interface in CMakeLists.txt using a C++ module file set:
cmake_minimum_required(VERSION 3.28)
project(ModuleExample LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(ModuleExample)
target_sources(ModuleExample
PRIVATE
main.cpp
PUBLIC
FILE_SET CXX_MODULES
FILES
math_utils.cppm
)
The CXX_MODULES file set tells CMake that the listed file is a C++ module interface unit. CMake can use this information during dependency scanning and build generation.
The example assumes a compatible compiler and generator. If configuration or compilation fails, inspect the first relevant error rather than assuming the module file set itself is incorrect.
Build the project using a separate build directory:
cmake -S . -B build
cmake --build build
Keeping generated files in a separate directory makes it easier to remove stale build artifacts without touching source files.
Debugging Module Dependency Errors
Module Not Found
A common error indicates that the compiler cannot find a module being imported.
For example:
fatal error: module 'math_utils' not found
Possible causes include:
The module interface is missing from the CMake target.
The file is not declared in a CXX_MODULES file set.
The compiler or generator does not support the required module workflow.
The module name in the import statement does not match the provided module name.
The project is using stale generated build files.
Start by checking the module declaration:
export module math_utils;
Then confirm that the consumer imports the same name:
import math_utils;
Next, inspect the CMake target and confirm that the module interface is registered correctly. If you recently changed the target configuration, regenerate the build directory.
cmake -S . -B build
cmake --build build --verbose
The verbose build output can reveal whether the expected source files are being compiled and which compiler commands CMake generates.
Do not immediately add manual compiler-specific module search paths. CMake-managed module dependencies should normally be handled through the build system when the compiler and generator support the configured workflow.
Module Dependency Scanning Fails
CMake needs to identify module providers and consumers to establish the correct build order.
Consider two modules:
// core.cppm
export module core;
export int get_value()
{
return 42;
}
// reporting.cppm
export module reporting;
import core;
export int get_report_value()
{
return get_value();
}
The reporting module depends on core. CMake must discover that relationship before the relevant compilation steps run.
If scanning fails, investigate the following:
Confirm that both module interfaces are registered in the appropriate CMake target or targets.
Check whether the compiler supports module dependency scanning for the selected build configuration.
Review the first scanning error in the build output.
Look for malformed module declarations or unsupported language features.
Verify that the configured generator is compatible with the compiler's module workflow.
A dependency scanning failure can be caused by an earlier syntax error in the module interface. Fix the first compiler or scanner error before investigating later messages about missing dependencies.
Incorrect Build Order
A module consumer cannot compile successfully until its imported module interface is available.
In a manually scripted build, developers sometimes try to solve this by compiling the provider first using a fixed command sequence. That may work for a small example but becomes difficult to maintain when dependencies grow.
CMake's module dependency scanning is designed to establish the necessary ordering. Avoid relying on alphabetical source-file ordering or assumptions about which source file will compile first.
If a consumer starts compiling before its provider's module information is ready, check whether the module provider was declared correctly and whether the build tool is using the generated dependency information.
Also avoid mixing CMake-managed module builds with manually generated module artifacts unless you have a specific, documented reason. Compiler-specific artifacts may not be portable between compiler versions, build configurations, or machines.
Using Verbose Builds and Clean Reconfiguration
When a module build fails, the first error is usually more useful than the final summary.
Run a verbose build:
cmake --build build --verbose
Look for:
The compiler used for each source file.
The selected language standard.
The module interface source files included in the build.
Dependency scanning commands and their reported failures.
Unexpected compiler flags.
References to generated build directories or stale module artifacts.
If the project has changed significantly, create a fresh build directory:
cmake -S . -B build-clean
cmake --build build-clean --verbose
This helps determine whether the failure comes from stale generated files or the current project configuration.
If the clean build succeeds but the original build fails, investigate differences in cached configuration, generator selection, compiler paths, and environment variables.
A clean build should be a diagnostic step, not a substitute for understanding the root cause.
Common Mistakes to Avoid
Treating Module Interfaces Like Ordinary Headers
A module interface is not simply another header file. It has language-level semantics and creates dependencies that the compiler and build system must understand.
Register module interface files through the appropriate CMake module file-set mechanism instead of treating them as ordinary implementation files.
Assuming Every Compiler Supports the Same Features
C++ modules support differs across compilers and versions. A project may require a particular compiler release or generator configuration.
Document the supported toolchain and test it in continuous integration.
Reusing Generated Build Artifacts Across Toolchains
Compiled module artifacts are compiler-specific. Reusing them after switching compiler versions, build configurations, or toolchains can cause confusing errors.
Use separate build directories for different toolchains and configurations.
Hiding the First Error
A build can produce many follow-up errors after a module provider fails to compile. Fix the earliest meaningful error before addressing downstream messages.
Adding Dependencies Without Checking the Module Graph
A module that imports another module introduces a dependency. As a project grows, circular or poorly organized dependencies make the build harder to understand.
Keep module boundaries clear and avoid unnecessary cross-dependencies.
Best Practices for Reliable Module Builds
Use these practices when introducing modules into an existing C++ codebase:
Start with one small module and one consumer before migrating an entire library.
Declare module interfaces explicitly in CMake.
Set the C++ language standard at the project or target level.
Verify compiler and generator compatibility before adopting modules in production.
Keep generated artifacts separate from source files.
Use verbose build output to investigate dependency scanning and compiler failures.
Test clean builds in continuous integration.
Keep module dependencies directed and understandable.
Document the supported compiler, CMake, and generator versions.
Avoid compiler-specific workarounds until the build configuration has been checked.
A gradual migration also helps teams identify which parts of the existing project benefit from modules and which should remain unchanged until the toolchain is ready.
Advantages and Limitations
C++ modules can reduce repeated parsing of declarations, provide clearer interfaces, and improve dependency management. They can also make large projects easier to organize when module boundaries are designed carefully.
However, adopting modules requires compatible compiler and build-system support. Existing header-based libraries may need additional integration work, and compiler-specific module artifacts complicate build caching and toolchain changes.
The performance benefits also depend on the project's structure, compiler implementation, and build workload. Moving code to modules does not automatically guarantee faster builds.
For an existing project, measure build performance before and after migration rather than assuming that the language feature alone will deliver a specific improvement.
Summary
Debugging C++ module dependencies in CMake starts with verifying toolchain compatibility, declaring module interfaces correctly, and understanding the dependency between module providers and consumers.
CMake's module scanning can establish the build order, but it depends on a supported compiler and generator configuration. When a build fails, inspect the first meaningful error, use verbose output, and test with a clean build directory if stale artifacts may be involved.
A small, well-configured module example is often the fastest way to isolate a problem. Once that works consistently across local builds and continuous integration, the same approach can be applied to larger parts of the project.
Join the conversation! Your thoughts help the community grow.