C++20 introduced modules as a powerful feature to replace the traditional header‑include model. Modules help reduce compile times, prevent name clashes, and enable cleaner interfaces. In this article we’ll walk through the basics of creating and consuming a module, discuss its benefits, and provide a concrete example of a simple math library that is exported as a module.
1. Why Modules?
Traditional C++ compilation relies on preprocessor directives: #include <foo.h>. Each translation unit that includes foo.h re‑parses the header, leading to long compile times and the “One Definition Rule” headaches. Modules solve this by:
- Explicit boundaries – The module interface is a single compilation unit, and its implementation can be hidden.
- Fast incremental builds – Once a module is compiled, other units import the compiled module, avoiding repeated parsing.
- Namespace isolation – Symbols are automatically scoped to the module, reducing accidental name clashes.
2. Module Syntax Overview
A module consists of two parts:
- Module interface unit – Declares the public API.
- Module implementation unit – Provides the definitions.
The syntax uses the module keyword:
// mathmodule.cppm (module interface unit)
export module mathmodule; // Declare the module name
export namespace math {
export int add(int a, int b);
}
Implementation can be in the same file or a separate file with the same module name:
// mathmodule_impl.cppm (module implementation unit)
module mathmodule; // Re-open the module
int math::add(int a, int b) {
return a + b;
}
3. Building a Module
To compile a module you need a compiler that supports C++20 modules. For example with GCC 12+ or Clang 14+, use:
# Compile the module interface
g++ -std=c++20 -fmodules-ts -c mathmodule.cppm -o mathmodule.o
# Compile the module implementation (if separate)
g++ -std=c++20 -fmodules-ts -c mathmodule_impl.cppm -o mathmodule_impl.o
# Link them into a shared library or static lib
ar rcs libmathmodule.a mathmodule.o mathmodule_impl.o
If you compile both parts together, the compiler automatically produces a module interface file .pcm for reuse.
4. Consuming a Module
In your consumer source file, use import instead of #include:
import mathmodule; // Import the entire module
#include <iostream>
int main() {
std::cout << "3 + 4 = " << math::add(3, 4) << '\n';
}
Compile with:
g++ -std=c++20 -fmodules-ts -c consumer.cpp -o consumer.o
g++ consumer.o -L. -lmathmodule -o consumer
Notice we link against libmathmodule.a. The compiler automatically loads the compiled module interface from the .pcm file generated earlier.
5. Practical Benefits
| Issue | Traditional Header Model | Modules |
|---|---|---|
| Compile time | Re‑parses each header per TU | Parses once, reuses compiled interface |
| Name clashes | Global namespace pollution | Names are scoped to module |
| Binary compatibility | Requires careful header changes | Module interface files (.pcm) capture ABI |
| Tooling | Header-only | Explicit boundaries aid IDEs and build systems |
6. Common Pitfalls
- Mixing
#includewithimport– Avoid including headers that are part of a module; useimportinstead. - Header duplication – Do not expose implementation details via headers that are also part of a module.
- Compiler support – Not all compilers fully support modules; check the documentation and use the
-fmodules-tsflag where necessary. - Binary ABI – If you change the module interface, all consumers must be recompiled; the
.pcmfile encodes the interface signature.
7. Extending the Example: A Small Math Module
Below is a complete example that demonstrates both interface and implementation in a single file, and shows how to compile and run the consumer.
mathmodule.cppm
export module mathmodule;
export namespace math {
export int add(int a, int b);
export double sqrt(double x);
}
int math::add(int a, int b) {
return a + b;
}
double math::sqrt(double x) {
return std::sqrt(x);
}
consumer.cpp
import mathmodule;
#include <iostream>
int main() {
std::cout << "add(5, 7) = " << math::add(5, 7) << '\n';
std::cout << "sqrt(9.0) = " << math::sqrt(9.0) << '\n';
}
Build commands
# Build the module interface
g++ -std=c++20 -fmodules-ts -c mathmodule.cppm -o mathmodule.o
# Build the consumer
g++ -std=c++20 -fmodules-ts consumer.cpp -L. -lstdc++ -o consumer
# Run
./consumer
The output should be:
add(5, 7) = 12
sqrt(9.0) = 3
8. Future Outlook
C++ modules are still evolving. Upcoming standards (C++23 and beyond) aim to improve module portability, integration with package managers, and better support for precompiled headers. As the ecosystem matures, you’ll see more libraries offering module-enabled builds, and build systems (CMake, Meson) gaining robust module support.
9. Takeaway
Modules are a significant leap forward in C++ packaging and compilation. They provide a clean, efficient, and type-safe way to separate interface from implementation. While the initial learning curve can be steep, the long-term payoff—especially for large codebases—makes mastering modules a worthwhile investment for every modern C++ developer.