By the end of this chapter you can
- Write and consume a module
- Explain what modules fix about headers
- Assess whether your toolchain can use them yet
Chapter 9.1 described #include as it is: textual pasting, performed before the
compiler sees anything, with every consequence that implies. Chapter 7.6
measured one of those consequences — an empty program taking 1,024 ms to compile
because of headers it does not use.
Modules replace the whole mechanism. A module is compiled once into a binary artefact describing its interface, and importing it loads that artefact instead of re-parsing text.
Writing one
A module interface unit — conventionally .cppm, though GCC accepts anything
you tell it is C++:
// math.cppm
export module math;
export int add(int a, int b) { return a + b; }
export int square(int n) { return n * n; }
int helper(int n) { return n + 1; } // not exported
export int bumped(int n) { return helper(n); }And a consumer:
// main.cpp
import math;
#include <cstdio>
int main() {
std::printf("%d %d %d\n", add(2, 3), square(5), bumped(9));
}Built with GCC 13:
$ g++ -std=c++20 -fmodules-ts -c -x c++ math.cppm -o math.o
$ g++ -std=c++20 -fmodules-ts -c main.cpp -o main.o
$ g++ math.o main.o -o app
$ ./app
5 25 10The interface unit must be compiled before anything that imports it —
compiling math.cppm writes gcm.cache/math.gcm, and import math; reads it.
That ordering constraint is new: with headers, translation units could be
compiled in any order, and with modules they cannot. It is the single biggest
thing a build system has to learn.
What export actually does
helper is in the module and not exported. It is not private, not static, and
not in an anonymous namespace — it simply is not part of the interface. A
consumer cannot see it:
main.cpp: In function 'int main()':
main.cpp:2:20: error: 'helper' was not declared in this scope
2 | int main(){ return helper(1); }This is genuinely new. With headers, “internal” means “in a .cpp file” or “in
an anonymous namespace” or “named detail::” — conventions, all of them
bypassable. A module’s boundary is enforced by the language.
The four things modules fix
Macros do not escape. A macro defined inside a module is not visible to importers, at all:
// mac.cppm
module;
#include <cstdio>
export module mac;
#define SECRET 42
export int reveal() { return SECRET; }$ ./macapp
42
SECRET did not leakreveal() returns 42, and #ifdef SECRET in the importing file is false. With
a header, SECRET would be defined in every file that included it and every
file those files included, forever. This alone removes an entire category of bug
— the min/max macros in <windows.h>, the assert that some header
redefined, the #define private public someone thought was clever.
Order stops mattering. Because there is no textual paste, import a; import b; and import b; import a; are identical. Headers that only work when
included after another header cannot exist.
One definition, once. A module interface is compiled once. Ten translation units importing it do not each re-parse and re-instantiate its templates.
Build time. Measured here, #include <vector> and <map> against importing
both as header units:
| Time | Preprocessed lines | |
|---|---|---|
#include <vector> <map> |
319 ms | 30,818 |
import <vector>; import <map>; |
195 ms | — |
About 40% faster for two headers. The .gcm files are large — 3.0 MB for
<vector>, 2.9 MB for <map> — because they hold a full parsed representation
rather than text, and that is the trade: disk and a build-ordering constraint,
in exchange for not re-parsing.
Partitions
A large module can be split without exposing the split to consumers.
// geo-shapes.cppm
export module geo:shapes;
export struct Point { double x, y; };
export double unit_area() { return 1.0; }// geo.cppm
export module geo;
export import :shapes; // re-export the partition
export double doubled_area() { return unit_area() * 2; }A consumer writes import geo; and gets Point, unit_area and
doubled_area. It cannot write import geo:shapes; — partitions are visible
only within their own module. Built and run, that prints 1 2 2.
This is the piece headers never had: a way to organise a component internally without that organisation becoming part of its public surface.
Whether you can use them yet
This is the objective that matters, and the honest answer for GCC 13.3 — released in 2024, and what Ubuntu 24.04 ships — is partly.
| Result | |
|---|---|
Named modules (export module) |
works |
| Module partitions | works |
| Non-exported entities hidden | works |
| Macros contained | works |
A header unit (import <vector>;) |
works, after pre-building it |
| Two header units in one file | works |
<vector>, <string> and <map> together |
internal compiler error |
import std; |
not available |
| CMake 3.28 module support with GCC 13 | refuses to configure |
The compiler crash is worth quoting in full, because it is what using this feature in 2024 actually looks like:
/usr/include/c++/13/bits/allocator.h:193:39: internal compiler error:
in make_decl_rtl, at varasm.cc:1442
Please submit a full bug report, with preprocessed sourceThree standard headers imported as header units, in one translation unit. Any two of the three are fine. Nothing about the program is unusual.
And the build system, which is the other half of usable:
CMake Error in CMakeLists.txt:
The target named "mathmod" has C++ sources that may use modules, but the
compiler does not provide a way to discover the import graph dependencies.CMake needs to know which module each file imports before compiling anything, in order to compile them in the right order. That requires the compiler to offer a dependency-scanning mode, and GCC 13 does not — so CMake declines to generate a build at all.
Migrating, when the time comes
You do not have to choose all at once. Three intermediate positions, in order of effort:
Import your own headers as header units. import "myheader.h"; treats an
existing header as a module without changing it. Nothing in the header needs to
be rewritten, and consumers get macro isolation and no re-parsing.
Wrap a library in a module. A module interface that includes the old headers in its global module fragment and re-exports what it needs:
module;
#include "legacy/everything.h" // global module fragment: ordinary includes
export module mylib;
export using legacy::Widget;
export using legacy::make_widget;Consumers get a module; the implementation is unchanged.
Write new components as modules. A module can #include headers and a
header cannot import a module cleanly, so the boundary between old and new
code has a direction: new code may depend on old, not the reverse.