C++20 introduced modules as a modern alternative to the traditional header-based inclusion model. This article walks through the benefits of modules, how to set them up in a typical build system, and common pitfalls to avoid.
Why Modules Matter
- Compile-time performance: Modules eliminate the need to recompile the same header files across translation units, dramatically reducing compilation times in large codebases.
- Name‑space hygiene: The compiler enforces module boundaries, preventing accidental name collisions that are common with headers.
- Improved encapsulation: Only explicitly exported symbols are visible to consumers, enabling tighter control over API exposure.
Basic Module Syntax
A module declaration appears at the top of a source file before any other code:
export module mylib.math;
export namespace math {
export double add(double a, double b) { return a + b; }
}
The export keyword before the module name indicates that this file defines a module. All symbols that need to be visible outside the module are prefixed with export.
Importing Modules
Consuming code uses the import directive:
import mylib.math;
#include <iostream>
int main() {
std::cout << math::add(3.0, 4.0) << '\n';
}
Note that import can only appear at the top level of a translation unit, similar to #include.
Build System Integration
Using CMake is the most straightforward way to enable modules:
cmake_minimum_required(VERSION 3.22)
project(MyLib LANGUAGES CXX)
add_library(mylib_math STATIC mylib_math.cpp)
target_compile_features(mylib_math PUBLIC cxx_std_20)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib_math)
The key part is target_compile_features(mylib_math PUBLIC cxx_std_20) which ensures the compiler is invoked with the correct language level.
Practical Tips
- Keep module boundaries clear: Group related functionality together. Avoid placing unrelated headers in the same module.
- Avoid
#includeinside modules: Prefer usingimportfor other modules or standard library headers (`import ;`). - Use precompiled headers sparingly: Modules already provide a precompilation benefit; mixing them with PCH can lead to confusion.
- Be aware of toolchain support: GCC 11+ and Clang 13+ provide solid support, but MSVC’s module support is still evolving.
Common Pitfalls
- Undefined symbols: Forgetting to export a symbol that is used elsewhere will lead to linker errors.
- Module aliasing conflicts: Using the same module name across different directories can cause ambiguous module paths.
- Header dependencies: If a module header is included in a consumer, the compiler treats it as a regular header, potentially negating the benefits of modules.
Conclusion
C++20 modules are a powerful feature that can streamline large-scale C++ projects, improving build times and code quality. By carefully defining module boundaries, exporting only the necessary symbols, and integrating them into your build system, you can harness the full potential of modules while keeping your codebase maintainable.