C++ Module 17 – Modules and coroutines
C++20 modules: export, import and partitions
Write a C++20 module with export and import, split it into partitions, and build it by hand with Clang and GCC 16, then with CMake and Ninja.
What you will learn
- Write a module interface unit and import it from another file
- Split a module into partitions and an implementation unit
- Build a module in the right order with Apple clang and GCC 16
- Move a module's #include lines into its global module fragment
- Describe a module library to CMake with a CXX_MODULES file set
Before you start
What you’ll learn · Before you start
What you will learn
- Write a module interface unit and import it from another file
- Split a module into partitions and an implementation unit
- Build a module in the right order with Apple clang and GCC 16
- Move a module's #include lines into its global module fragment
- Describe a module library to CMake with a CXX_MODULES file set
Before you start
On this page
A header is pasted text. Every .cpp file that includes it makes the compiler read and parse it again, so in a
project of 300 files a common header is parsed 300 times, and a macro defined before the #include can change what
it means. A C++20 named module: A part of a program declared with export module name; and brought into other files with import name;. It is compiled once into a module file, and importers get only the declarations it exports: never its other names and never its macros. is compiled once: the file that starts with
export module geometry; is its interface, what it marks export is all that import geometry; gives another file,
the rest stays private to the module, macros included, and the compiler saves the interface in a
module file (BMI): The compiled form of a module interface that importers read instead of source text, such as geometry.pcm from Clang or gcm.cache/geometry.gcm from GCC. Clang's documentation calls it a built module interface, or BMI. It must exist before any importer is compiled. that importers read instead of text.
A large module can be split into partitions, such as geometry:shapes, that only the module’s own files import. You
will build one module with a partition and an implementation unit on Apple clang 21 and GCC 16, read what the
compilers say about the five mistakes that stop most first builds, and let CMake 4.4 work out the build order.
A header is pasted text; a module is compiled once
Every one of those 300 files also receives everything the header needed for itself: its helpers, the headers it
includes and its macros. That is how two libraries end up fighting over a macro called min or a function called
log. And when two files see a header’s definitions differently, because one of them defines a macro the other does
not, the program breaks the one-definition rule: The rule that a program holds exactly one definition of each ordinary function it uses. An inline function may be defined in every translation unit that uses it, as long as all the definitions are identical. More in “How C++ compiles: preprocess, compile and link” without any error. A module
changes each of these:
Header (#include) |
Module (import) |
|
|---|---|---|
| What the compiler reads | The header’s text, in every file that includes it | A module file, compiled once |
| Macros | Flow in both directions | Never leave the module and never enter it |
| Names that are not part of the interface | Visible to every includer | Private to the module (module linkage) |
| Order of inclusion or import | Can change the meaning | Does not matter |
| Build order | Any order | A module before every file that imports it |
The last row is the cost. Because an importer reads a module file, the module must be compiled first, and the build system has to know who imports whom. Most of this lesson is about meeting that rule.
Key idea
An #include pastes a header’s text into every file, with that file’s macros. A module is compiled once, gives its
importers only what it exports, and so must be built before every file that imports it.
Checkpoint
Check: Pasted text against a compiled module
export decides what an importer can use
The geometry module has four files. Look at all of them before reading the explanation below:
CPPM (geometry-shapes.cppm) · geometry/geometry-shapes.cppm
// The shapes partition: the types of the geometry module.
export module geometry:shapes;
export namespace geometry {
struct Rect {
double width = 0; // metres
double height = 0;
};
struct Circle {
double radius = 0;
};
} // namespace geometry CPPM (geometry.cppm) · geometry/geometry.cppm
// The primary interface of the geometry module: what an importer can use.
export module geometry;
export import :shapes; // the partition's types, passed on to every importer
#define GEOMETRY_VERSION 2 // a macro: visible in this file only, never exported
export namespace geometry {
double area(const Rect& r);
double area(const Circle& c);
double perimeter(const Rect& r);
} // namespace geometry
namespace geometry {
// Not exported: units of the geometry module may use these, importers may not.
inline constexpr double pi = 3.14159265358979;
double square(double x) { return x * x; }
} // namespace geometry C++ (geometry.cpp) · geometry/geometry.cpp
// An implementation unit of geometry: the function bodies. It sees everything the interface declares.
module geometry;
namespace geometry {
double area(const Rect& r) { return r.width * r.height; }
double area(const Circle& c) { return pi * square(c.radius); }
double perimeter(const Rect& r) { return 2 * (r.width + r.height); }
} // namespace geometry C++ (main.cpp) · geometry/main.cpp
#include <print>
import geometry;
int main() {
geometry::Rect plot{12.5, 8.0};
geometry::Circle well{1.2};
std::println("plot: {:.2f} sq m, fence {:.2f} m", geometry::area(plot), geometry::perimeter(plot));
std::println("well: {:.2f} sq m", geometry::area(well));
} geometry.cppm is the module interface unit: A file that starts with export module, as in export module geometry; or export module geometry:shapes;. The primary interface, the one without a partition name, exists once per module and decides what importers can use.: its first declaration,
export module geometry;, names the module and says this file is its interface. A module has exactly one. Inside it,
export namespace geometry { … } exports every declaration in that block, so importers can call area and
perimeter. The constant pi and the function square sit in a namespace block without export: they belong to
the module and have module linkage: What a name declared in a named module without export has: every file of that module may use it, and no file outside the module can., so every file of the module may use them and no
importer can. The macro GEOMETRY_VERSION lives only in that one file.
geometry.cpp starts with module geometry;, without export. That makes it a
module implementation unit: A file of a module that starts with module name; without export. It holds definitions, implicitly imports the primary interface and cannot be imported, so changing it does not change what importers read., the module’s equivalent of a .cpp file
next to a header. The standard says such a unit implicitly imports the primary interface, so it sees Rect, pi and
square without importing anything. The function bodies live here, and when one of them changes, only this file is
compiled again: the module file, and so every importer, stays as it was.
main.cpp is an ordinary file. It includes <print>, imports geometry and uses the exported names with their
namespace, as with any library; an #include and an import can sit in the same file. Step through which of the
four files can use which name:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The primary interface.
export module geometry;makesgeometry.cppmthe one file that says what the module offers. It declaresareaandperimeterinsideexport namespace geometry, andpiandsquarein a block withoutexport. - The partition passes types on. The partition
geometry:shapesexports the typesRectandCircle.export import :shapes;imports the partition into the primary interface and exports what it exported. - The module's own files see all.
geometry.cppstarts withmodule geometry;, so it imports the primary interface without saying so and sees every name in it, exported or not: its bodies usepiandsquare. - An importer sees the exports.
main.cppimportsgeometryand gets what is exported:area,perimeter,RectandCircle. It printedplot: 100.00 sq m, fence 41.00 mwith both compilers. - Module linkage stops here.
piandsquarehave module linkage: they never reach a file outside the module. Clang says ofgeometry::squarein such a file that its declaration must be imported from modulegeometrybefore it is required. - A macro stays in its file.
GEOMETRY_VERSIONis a macro ofgeometry.cppm, and no import of a named module carries a macro: neithermain.cppnor the module's owngeometry.cppcan use it. - Bodies change, importers stay. A changed function body in
geometry.cpprecompiles only that file. The interface, and so the module file thatmain.cppreads, stay as they were, somain.cppis not compiled again.
The exports travel along every import, and everything else stops at the edge of the module. That edge is what module linkage means, and the mistakes section below shows the error a file outside the module gets when it crosses it.
Checkpoint
Check: What export gives an importer
Partitions split one module into files that importers never see
geometry-shapes.cppm begins with export module geometry:shapes;. The part after the colon names a
module partition: A file of a module with a name after a colon, such as geometry:shapes. Only files of the same module can import it, with import :shapes;, and the primary interface must export every partition that is an interface unit.: a file that belongs to geometry but holds only a piece of it, here
the two types. Three rules come with partitions:
- Only the module’s own files can import a partition, with the short form
import :shapes;. To the rest of the program the division into partitions is invisible: it importsgeometryand nothing else. - Every interface partition must be exported by the primary interface, directly or through another partition.
That is what
export import :shapes;does:RectandCirclereach every importer ofgeometry. The standard requires no diagnostic when you forget it, so the mistake can show up as a confusing error far away. - A partition without
export, such asmodule geometry:detail;, is an internal partition: code that the module’s files share and that never becomes part of the interface.
Partitions are how a large module stays a set of reasonably sized files without exposing that split to its users. The build order follows the imports:
The geometry module's import graph is its build order
Text description of the diagram
The diagram shows the four files of the geometry program in the order a build must compile them, from top to bottom.
- geometry-shapes.cppm is the partition geometry:shapes. It imports nothing, so it is compiled first. Besides its object file it writes a module file: geometry-shapes.pcm with Clang, gcm.cache/geometry-shapes.gcm with GCC.
- geometry.cppm is the primary interface of geometry. It imports the partition with export import :shapes, so it can only be compiled once the partition's module file exists. It writes the module file of geometry.
- geometry.cpp, the implementation unit (module geometry;), and main.cpp (import geometry;) both read that module file. They do not depend on each other, so a build can compile them in parallel.
- The linker joins the four object files into the program, plot.
An import is an edge of the graph: a file can be compiled only after every module it imports. The graph has no cycles, which the language requires.
Checkpoint
Check: Partitions of one module
A module is compiled before every file that imports it
Here is the whole build with both compilers, file by file, in the order of the diagram:
from session import run
# Apple clang 21 builds standard modules only with -fcxx-modules. Each interface unit also writes its module file.
run("""
clang++ -std=c++23 -fcxx-modules -c geometry/geometry-shapes.cppm -fmodule-output=geometry-shapes.pcm -o geometry-shapes.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/geometry.cppm -fmodule-output=geometry.pcm -o geometry.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/geometry.cpp -o geometry-impl.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/main.cpp -o main.o
clang++ geometry-shapes.o geometry.o geometry-impl.o main.o -o plot
./plot
ls -1 *.pcm
# GCC 16 needs -fmodules, and writes its module files (.gcm) into gcm.cache/.
g++-16 -std=c++23 -fmodules -c geometry/geometry-shapes.cppm -o geometry-shapes.o
g++-16 -std=c++23 -fmodules -c geometry/geometry.cppm -o geometry.o
g++-16 -std=c++23 -fmodules -c geometry/geometry.cpp -o geometry-impl.o
g++-16 -std=c++23 -fmodules -c geometry/main.cpp -o main.o
g++-16 geometry-shapes.o geometry.o geometry-impl.o main.o -o plot
./plot
ls -1 gcm.cache
""") Output
$ clang++ -std=c++23 -fcxx-modules -c geometry/geometry-shapes.cppm -fmodule-output=geometry-shapes.pcm -o geometry-shapes.o $ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/geometry.cppm -fmodule-output=geometry.pcm -o geometry.o $ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/geometry.cpp -o geometry-impl.o $ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/main.cpp -o main.o $ clang++ geometry-shapes.o geometry.o geometry-impl.o main.o -o plot $ ./plot plot: 100.00 sq m, fence 41.00 m well: 4.52 sq m $ ls -1 *.pcm geometry-shapes.pcm geometry.pcm # GCC 16 needs -fmodules, and writes its module files (.gcm) into gcm.cache/. $ g++-16 -std=c++23 -fmodules -c geometry/geometry-shapes.cppm -o geometry-shapes.o $ g++-16 -std=c++23 -fmodules -c geometry/geometry.cppm -o geometry.o $ g++-16 -std=c++23 -fmodules -c geometry/geometry.cpp -o geometry-impl.o $ g++-16 -std=c++23 -fmodules -c geometry/main.cpp -o main.o $ g++-16 geometry-shapes.o geometry.o geometry-impl.o main.o -o plot $ ./plot plot: 100.00 sq m, fence 41.00 m well: 4.52 sq m $ ls -1 gcm.cache geometry-shapes.gcm geometry.gcm
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_session.py
Step through the Clang half of that session, one stage per command, with what each command wrote:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The partition first. The partition imports nothing, so it can be compiled first.
-fmodule-output=geometry-shapes.pcmmakes Clang write its module file beside the object file. - Then the interface. The primary interface imports the partition, so it can compile only now.
-fprebuilt-module-path=.lets Clang findgeometry-shapes.pcm, the colon ofgeometry:shapeswritten as a hyphen, and the interface writesgeometry.pcm. - Then every importer.
geometry.cppandmain.cppboth readgeometry.pcmand nothing else of the module. Neither imports the other, so a build may compile them at the same time. - The link. The linker joins the four object files into
plot. Module files are read only while compiling: none of them is passed to the link. - The run.
plotprintedplot: 100.00 sq m, fence 41.00 mandwell: 4.52 sq m, andls -1 *.pcmlisted the two module files,geometry-shapes.pcmandgeometry.pcm. - GCC keeps the order. GCC 16 needs
-fmodulesand no option for module files: it wrotegeometry-shapes.gcmandgeometry.gcmintogcm.cacheand found them there, in the same order, and itsplotprinted the same two lines.
What each flag does:
-fcxx-modules(Apple clang). Apple’s compiler readsexport moduleas an error unless this flag is given, even with-std=c++23. Upstream Clang, for example the LLVM 23 that Homebrew installs, needs no flag.-fmodule-output=geometry.pcm(Clang). Compiling an interface unit writes the object file and, as a by-product, the module file. Clang’s documentation calls this one-phase compilation and says it is the model CMake uses.-fprebuilt-module-path=.(Clang). Where to look for module files. Clang expectsgeometry.pcmfor the module andgeometry-shapes.pcmfor the partitiongeometry:shapes, the colon written as a hyphen.-fmodules(GCC). GCC 16’s release notes still call modules experimental and require this flag. GCC writes the module files into a folder calledgcm.cacheand finds them there again.
File names follow the compilers. Clang’s documentation asks for .cppm on interface units and partitions and .cpp on
implementation units. GCC accepts any C++ suffix, .cppm and .ixx included. Microsoft’s compiler uses .ixx for
interface units by default and the /interface option for other suffixes.
Do not test __cpp_modules
Both builds above compile modules, yet only one compiler says so through its
feature-test macro: A macro such as __cpp_modules or __cpp_lib_modules whose value, a year and month such as 202207, says that a compiler or its library claims a feature. A toolchain may build a feature without defining its macro, or define the macro without the feature.. Apple clang 21 never defines __cpp_modules, even with
-fcxx-modules, while GCC 16 defines it only with -fmodules, and to the older value 201810. Apple clang also
defines __cpp_lib_modules, the macro for import std;, although its library ships no std module to import. The
program below prints both macros as each compiler sets them with its modules flag.
// What the compiler says about modules through the feature-test macros of C++20 and C++23.
#include <cstdio>
#include <version>
int main() {
#ifdef __cpp_modules
std::printf("__cpp_modules = %ld (the language feature)\n", static_cast<long>(__cpp_modules));
#else
std::puts("__cpp_modules is not defined");
#endif
#ifdef __cpp_lib_modules
std::printf("__cpp_lib_modules = %ld (import std)\n", static_cast<long>(__cpp_lib_modules));
#else
std::puts("__cpp_lib_modules is not defined");
#endif
} from session import run
# Both builds below compile modules; only one compiler says so through __cpp_modules.
run("""
clang++ -std=c++23 -fcxx-modules features.cpp -o features && ./features
g++-16 -std=c++23 -fmodules features.cpp -o features && ./features
""") Output
$ clang++ -std=c++23 -fcxx-modules features.cpp -o features && ./features __cpp_modules is not defined __cpp_lib_modules = 202207 (import std) $ g++-16 -std=c++23 -fmodules features.cpp -o features && ./features __cpp_modules = 201810 (the language feature) __cpp_lib_modules = 202207 (import std)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 features_session.py
Read the two runs side by side: a test of __cpp_modules would call Apple clang unable to build modules, which it
has just done, and a test of __cpp_lib_modules would send it to an import std; that fails, as the next lesson
shows. To decide whether modules work, compile a small module in the build system’s configure step instead.
Checkpoint
Check: Building in the order of the imports
Five mistakes stop most first builds
Each command below makes one of the classic mistakes:
from session import run
run("""
# 1. Without the modules flag, both compilers read export module as ordinary code.
clang++ -std=c++23 -c geometry/geometry-shapes.cppm -o geometry-shapes.o
g++-16 -std=c++23 -c geometry/geometry-shapes.cppm -o geometry-shapes.o
# 2. An importer compiled before the module it imports.
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/main.cpp -o main.o
g++-16 -std=c++23 -fmodules -c geometry/main.cpp -o main.o
# The module built properly, then misuse.cpp: 3. a name that is not exported, 4. a macro of the interface.
clang++ -std=c++23 -fcxx-modules -c geometry/geometry-shapes.cppm -fmodule-output=geometry-shapes.pcm -o geometry-shapes.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/geometry.cppm -fmodule-output=geometry.pcm -o geometry.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/misuse.cpp -o misuse.o
g++-16 -std=c++23 -fmodules -c geometry/geometry-shapes.cppm -o geometry-shapes.o
g++-16 -std=c++23 -fmodules -c geometry/geometry.cppm -o geometry.o
g++-16 -std=c++23 -fmodules -c geometry/misuse.cpp -o misuse.o
# 5. A partition imported from outside its module.
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/partition_outside.cpp -o outside.o
g++-16 -std=c++23 -fmodules -c geometry/partition_outside.cpp -o outside.o
""") Output
# 1. Without the modules flag, both compilers read export module as ordinary code.
$ clang++ -std=c++23 -c geometry/geometry-shapes.cppm -o geometry-shapes.o
geometry/geometry-shapes.cppm:2:8: error: expected template
2 | export module geometry:shapes;
| ^
geometry/geometry-shapes.cppm:2:8: error: unknown type name 'module'
geometry/geometry-shapes.cppm:2:23: error: expected ';' after top level declarator
2 | export module geometry:shapes;
| ^
| ;
geometry/geometry-shapes.cppm:2:24: error: a type specifier is required for all declarations
2 | export module geometry:shapes;
| ^
geometry/geometry-shapes.cppm:4:8: error: expected template
4 | export namespace geometry {
| ^
geometry/geometry-shapes.cppm:4:18: error: redefinition of 'geometry' as different kind of symbol
4 | export namespace geometry {
| ^
geometry/geometry-shapes.cppm:2:15: note: previous definition is here
2 | export module geometry:shapes;
| ^
geometry/geometry-shapes.cppm:1:1: error: missing 'export module' declaration in module interface unit
1 | // The shapes partition: the types of the geometry module.
| ^
7 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -c geometry/geometry-shapes.cppm -o geometry-shapes.o
geometry/geometry-shapes.cppm:2:1: warning: keyword 'export' is enabled with '-fmodules'
2 | export module geometry:shapes;
| ^~~~~~
geometry/geometry-shapes.cppm:2:8: error: 'module' does not name a type
2 | export module geometry:shapes;
| ^~~~~~
geometry/geometry-shapes.cppm:2:8: note: C++20 'module' only available with '-fmodules', which is not yet enabled with '-std=c++20'
geometry/geometry-shapes.cppm:4:1: warning: keyword 'export' is enabled with '-fmodules'
4 | export namespace geometry {
| ^~~~~~
[exit status 1]
# 2. An importer compiled before the module it imports.
$ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/main.cpp -o main.o
geometry/main.cpp:2:8: fatal error: module 'geometry' not found
2 | import geometry;
| ~~~~~~~^~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fmodules -c geometry/main.cpp -o main.o
In module imported at geometry/main.cpp:2:1:
geometry: error: failed to read compiled module: No such file or directory
geometry: note: compiled module file is 'gcm.cache/geometry.gcm'
geometry: note: imports must be built before being imported
geometry: fatal error: returning to the gate for a mechanical issue
compilation terminated.
[exit status 1]
# The module built properly, then misuse.cpp: 3. a name that is not exported, 4. a macro of the interface.
$ clang++ -std=c++23 -fcxx-modules -c geometry/geometry-shapes.cppm -fmodule-output=geometry-shapes.pcm -o geometry-shapes.o
$ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/geometry.cppm -fmodule-output=geometry.pcm -o geometry.o
$ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c geometry/misuse.cpp -o misuse.o
geometry/misuse.cpp:5:29: error: declaration of 'square' must be imported from module 'geometry' before it is required
5 | double side = geometry::square(3.0); // declared in the module, but not exported
| ^
geometry/geometry.cppm:17:8: note: declaration here is not visible
17 | double square(double x) { return x * x; }
| ^
geometry/misuse.cpp:6:12: error: use of undeclared identifier 'GEOMETRY_VERSION'
6 | return GEOMETRY_VERSION; // a macro of the interface file
| ^~~~~~~~~~~~~~~~
2 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -fmodules -c geometry/geometry-shapes.cppm -o geometry-shapes.o
$ g++-16 -std=c++23 -fmodules -c geometry/geometry.cppm -o geometry.o
$ g++-16 -std=c++23 -fmodules -c geometry/misuse.cpp -o misuse.o
Text longer than 4,000 characters is cut here; run the example yourself to see all of it.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 mistakes_session.py
Reading the messages:
- The modules flag is missing. Apple clang reports
unknown type name 'module', because without-fcxx-modulesthe word is an ordinary name. GCC is kinder: its note says that C++20moduleis only available with-fmodules. - The importer was compiled first. Clang says
module 'geometry' not found; GCC names the file it looked for,gcm.cache/geometry.gcm, and adds that imports must be built before being imported. Compile in the order of the import graph, or let a build system work it out. - A name that is not exported. Clang’s message is the precise one: the declaration of
squareexists, but it is not visible outside the module. GCC simply reports thatsquareis not a member ofgeometry. - A macro of the interface. Both compilers report an undeclared identifier: macros never cross an import. A
value that importers need should be an exported
constexprvariable instead. - A partition imported from outside.
import geometry:shapes;is not valid syntax outside the module, so both compilers stop with a parse error at the colon. Import the module, which passes the partition on.
Checkpoint
Check: Reading the five first-build errors
A module’s headers go in its global module fragment
Real modules need the standard library and other headers. They go into a
global module fragment: The start of a module unit between module; and the module declaration. Only preprocessor directives such as #include may appear in it, and what they declare belongs to the global module, the module of every ordinary file. at the very top of the file, between module; and
the module declaration:
CPPM (greeting.cppm) · gmf/greeting.cppm
// The headers a module needs go into its global module fragment, before the module declaration.
module;
#include <string>
export module greeting;
export std::string greet(const std::string& name) { return "Namaste, " + name + "!"; } C++ · gmf/hello.cpp
#include <print>
#include <string>
import greeting;
int main() { std::println("{}", greet("Kavya")); } CPPM (greeting_wrong.cppm) · gmf/greeting_wrong.cppm
// The same module with the #include moved below the module declaration: a mistake.
export module greeting;
#include <string>
export std::string greet(const std::string& name) { return "Namaste, " + name + "!"; } Only preprocessor directives may appear in that fragment. Declarations that come from it belong to the global module,
the same one that ordinary files use, so the std::string that greet returns is the very type that hello.cpp
gets from <string>. In greeting_wrong.cppm the #include comes after export module, so everything the header
declares would be attached to the module greeting, as a second, private copy of the standard library:
from session import run
run("""
clang++ -std=c++23 -fcxx-modules -c gmf/greeting.cppm -fmodule-output=greeting.pcm -o greeting.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. gmf/hello.cpp greeting.o -o hello && ./hello
g++-16 -std=c++23 -fmodules gmf/greeting.cppm gmf/hello.cpp -o hello && ./hello
# The #include moved after export module:
clang++ -std=c++23 -fcxx-modules -c gmf/greeting_wrong.cppm -fmodule-output=greeting.pcm -o greeting.o
g++-16 -std=c++23 -fmodules -c gmf/greeting_wrong.cppm -o greeting.o
""", max_lines=14) Output
$ clang++ -std=c++23 -fcxx-modules -c gmf/greeting.cppm -fmodule-output=greeting.pcm -o greeting.o
$ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. gmf/hello.cpp greeting.o -o hello && ./hello
Namaste, Kavya!
$ g++-16 -std=c++23 -fmodules gmf/greeting.cppm gmf/hello.cpp -o hello && ./hello
Namaste, Kavya!
# The #include moved after export module:
$ clang++ -std=c++23 -fcxx-modules -c gmf/greeting_wrong.cppm -fmodule-output=greeting.pcm -o greeting.o
gmf/greeting_wrong.cppm:4:10: warning: '#include <filename>' attaches the declarations to the named module 'greeting', which is not usually intended; consider moving that directive before the module declaration [-Winclude-angled-in-module-purview]
4 | #include <string>
| ^
In file included from gmf/greeting_wrong.cppm:4:
In file included from …/c++/v1/string:596:
In file included from …/c++/v1/__algorithm/remove.h:12:
In file included from …/c++/v1/__algorithm/find.h:35:
In file included from …/c++/v1/cwchar:118:
In file included from …/c++/v1/wchar.h:116:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/wchar.h:67:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_wchar.h:92:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/time.h:63:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_time.h:111:39: error: declaration of 'tm' in the global module follows declaration in module greeting
111 | char *_LIBC_CSTR asctime(const struct tm *);
[94 more lines]
[exit status 1]
$ g++-16 -std=c++23 -fmodules -c gmf/greeting_wrong.cppm -o greeting.o
In file included from …/c++/16/bits/stl_algobase.h:63,
from …/c++/16/string:55,
from gmf/greeting_wrong.cppm:4:
…/c++/16/bits/stl_pair.h:102:12: error: conflicting declaration of 'struct std::_Index_tuple<_Indexes>' in module 'greeting'
102 | struct _Index_tuple;
| ^~~~~~~~~~~~
In file included from …/c++/16/bits/char_traits.h:52,
from …/c++/16/string:45:
…/c++/16/type_traits:4372:39: note: previously declared in global module
4372 | template<size_t... _Indexes> struct _Index_tuple { };
| ^~~~~~~~~~~~
In file included from …/c++/16/string:83:
…/c++/16/bits/memory_resource.h: In constructor 'std::pmr::polymorphic_allocator<_Tp>::polymorphic_allocator()':
…/c++/16/bits/memory_resource.h:138:33: error: block-scope extern declaration 'std::pmr::memory_resource* std::pmr::get_default_resource()' must not be attached to a named module [-Wtemplate-body]
[2 more lines]
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 gmf_session.py
Step through which module each declaration of the two files belongs to:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The fragment opens.
module;on the first line ofgreeting.cppmopens the global module fragment. Only preprocessor directives, such as#include, may appear in it. - A header in the fragment.
#include <string>pastes the header here, and everything it declares,std::stringincluded, belongs to the global module: the module of every ordinary file. - The purview starts.
export module greeting;ends the fragment. From this line on the file is the purview of the module, and what it declares is attached togreeting. - greet belongs to greeting.
greetis declared in the purview and exported, so it is attached togreeting. Thestd::stringit returns is the global one, from the fragment. - The importer agrees.
hello.cppincludes<string>itself and gets the samestd::stringfrom the global module, so the two files agree on the type. Both compilers built it, and it printedNamaste, Kavya!. - The include below the line.
greeting_wrong.cppmhas the same#includebelowexport module greeting;. Everything the header declares would then be attached togreeting: a second, private copy of the standard library. - The two copies clash. Clang warned that the
#includeattaches the declarations togreeting, then failed becausetmwas declared in both modules; GCC reported a conflicting declaration in modulegreeting.
Clang names the mistake in its first warning and then fails on the clash between the module’s copy of the C library
declarations and the global one; GCC reports the same clash as a conflicting declaration in module greeting.
An #include below export module
export module greeting; followed by #include <string> attaches what the header declares to the module, and
both compilers then fail on the clash with the global copy. Put module; on the first line and every #include
between it and export module greeting;, as greeting.cppm does.
Checkpoint
Check: Headers in the global module fragment
CMake scans every file for imports before it compiles
CMake learns which files are module interfaces from a file set of type CXX_MODULES. Implementation units such as
geometry.cpp are ordinary sources:
cmake_minimum_required(VERSION 3.28)
project(geometry_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(geometry)
target_sources(geometry
PUBLIC
FILE_SET CXX_MODULES FILES # interface units, partitions included
geometry.cppm
geometry-shapes.cppm
PRIVATE
geometry.cpp # the implementation unit is an ordinary source
)
add_executable(plot main.cpp)
target_link_libraries(plot PRIVATE geometry) CMake’s documentation lists what its module support needs:
- Generators: Ninja 1.11 or newer, Ninja Multi-Config, and Visual Studio 2022 or newer. The Makefile generators do not support modules.
- Compilers: Clang 16 or newer, GCC 14 or newer, and MSVC 14.34 or newer (Visual Studio 17.4).
Here is the project configured four ways on a Mac, with CMake 4.4.4 and Ninja 1.13.2, and built twice:
from cmake_tools import run
# CMake 4.4.4 and Ninja 1.13.2 with GCC 16: the build scans every file for imports, then compiles in their order.
run("""
cd geometry
cmake -S . -B build-gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16
""", keep=r"^-- (The CXX compiler identification|Build files)")
run("""
cmake --build build-gcc
./build-gcc/plot
# Homebrew's LLVM 23 scans with its own clang-scan-deps:
cmake -S . -B build-llvm -G Ninja -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++
""", keep=r"^(\[|plot|well|-- (The CXX compiler identification|Build files))")
run("""
cmake --build build-llvm
./build-llvm/plot
""", keep=r"^(\[12/|plot|well)")
run("""
# Apple clang 21, and the Unix Makefiles generator, stop at the generate step:
cmake -S . -B build-clt -G Ninja -DCMAKE_CXX_COMPILER=clang++
""", keep=r"^(-- The CXX compiler identification|CMake Error|CMake Generate| +\S)")
run("""
# The scanner CMake runs for upstream Clang is not part of the Command Line Tools:
xcrun --find clang-scan-deps
""")
run("""
cmake -S . -B build-make -G "Unix Makefiles" -DCMAKE_CXX_COMPILER=g++-16
""", keep=r"^(-- The CXX compiler identification|CMake Error|CMake Generate| +\S)") Output
$ cd geometry
$ cmake -S . -B build-gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16
-- The CXX compiler identification is GNU 16.2.0
-- Build files have been written to: build-gcc
[11 lines not shown]
$ cmake --build build-gcc
[1/12] Scanning geometry.cpp for CXX dependencies
[2/12] Scanning geometry.cppm for CXX dependencies
[3/12] Scanning geometry-shapes.cppm for CXX dependencies
[4/12] Generating CXX dyndep file CMakeFiles/geometry.dir/CXX.dd
[5/12] Scanning main.cpp for CXX dependencies
[6/12] Building CXX object CMakeFiles/geometry.dir/geometry-shapes.cppm.o
[7/12] Building CXX object CMakeFiles/geometry.dir/geometry.cppm.o
[8/12] Building CXX object CMakeFiles/geometry.dir/geometry.cpp.o
[9/12] Generating CXX dyndep file CMakeFiles/plot.dir/CXX.dd
[10/12] Linking CXX static library libgeometry.a
[11/12] Building CXX object CMakeFiles/plot.dir/main.cpp.o
[12/12] Linking CXX executable plot
$ ./build-gcc/plot
plot: 100.00 sq m, fence 41.00 m
well: 4.52 sq m
# Homebrew's LLVM 23 scans with its own clang-scan-deps:
$ cmake -S . -B build-llvm -G Ninja -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++
-- The CXX compiler identification is Clang 23.1.2
-- Build files have been written to: build-llvm
[7 lines not shown]
$ cmake --build build-llvm
[12/12] Linking CXX executable plot
[11 lines not shown]
$ ./build-llvm/plot
plot: 100.00 sq m, fence 41.00 m
well: 4.52 sq m
# Apple clang 21, and the Unix Makefiles generator, stop at the generate step:
$ cmake -S . -B build-clt -G Ninja -DCMAKE_CXX_COMPILER=clang++
-- The CXX compiler identification is AppleClang 21.0.0.21000334
CMake Error in CMakeLists.txt:
The target named "geometry" has C++ sources that may use modules, but the
compiler does not provide a way to discover the import graph dependencies.
See the cmake-cxxmodules(7) manual for details. Use the
CMAKE_CXX_SCAN_FOR_MODULES variable to enable or disable scanning.
CMake Generate step failed. Build files cannot be regenerated correctly.
[9 lines not shown]
[exit status 1]
# The scanner CMake runs for upstream Clang is not part of the Command Line Tools:
$ xcrun --find clang-scan-deps
xcrun: error: unable to find utility "clang-scan-deps", not a developer tool or in PATH
[exit status 72]
$ cmake -S . -B build-make -G "Unix Makefiles" -DCMAKE_CXX_COMPILER=g++-16
-- The CXX compiler identification is GNU 16.2.0
CMake Error in CMakeLists.txt:
The target named "geometry" has C++ sources that may use modules, but
modules are not supported by this generator:
Unix Makefiles
Modules are supported only by Ninja, Ninja Multi-Config, and Visual Studio
generators for VS 17.4 and newer. See the cmake-cxxmodules(7) manual for
details. Use the CMAKE_CXX_SCAN_FOR_MODULES variable to enable or disable
scanning.
CMake Generate step failed. Build files cannot be regenerated correctly.
[15 lines not shown]
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cmake_session.py
Step through the GCC build one stage of Ninja’s log at a time, then the two configures that stopped:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- Configure.
cmake -S . -B build-gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16found GNU 16.2.0. TheCXX_MODULESfile set told CMake which two files are module interfaces. - Generate. The generate step wrote
build.ninja, with a scan rule for every source of the two C++23 targets. The imports are not known yet, so CMake leaves the order of the compiles to the build. - Scan. Ninja's steps
[1/12]to[3/12]asked GCC which modules the three files ofgeometryprovide and import, and[5/12]asked the same aboutmain.cpp. Nothing was compiled yet. - Collate.
[4/12]collated the scans ofgeometryintoCXX.dd, the file that tells Ninja which compile needs which module file, and[9/12]wrote the one ofplot. - Compile in order.
[6/12]compiled the partition,[7/12]the primary interface and[8/12]the implementation unit: the order of the import graph, althoughCMakeLists.txtlistsgeometry.cppmbefore the partition. - Archive and link.
[10/12]archivedlibgeometry.a,[11/12]compiledmain.cpp, which needed the module file ofgeometry, and[12/12]linkedplot. - Run.
plotprintedplot: 100.00 sq m, fence 41.00 mandwell: 4.52 sq m, as the hand build did. Homebrew's LLVM 23 built the same project in 12 steps too. - Apple clang stops. With Apple clang 21, CMake stopped at the generate step: the compiler does not provide a way to discover the import graph dependencies.
xcrun --find clang-scan-depsfound no scanner in the Command Line Tools either. - Makefiles stop too. With the Unix Makefiles generator and GCC 16, CMake stopped at the same step. Its message says modules are supported only by the Ninja, Ninja Multi-Config and Visual Studio generators.
Before compiling anything, the build ran a dependency scan: A build step in which the compiler reads a source file only to report which modules it provides and imports. CMake runs one for every source of a C++20 target, then orders the compiles so that each module is built before its importers. of every source, and the
scans alone decided the order: the partition was compiled first, although CMakeLists.txt names it second. Apple
clang is not on the documentation’s list. CMake asks upstream Clang for a file’s imports through a separate tool,
clang-scan-deps, which the Command Line Tools do not include (xcrun --find in the session finds none), and it has
no scanning rules for Apple’s compiler, so it stopped at the generate step with the message in the session. The
Makefile generator was refused in the same way. Header units, import <vector>;, are not supported by CMake at all.
Key idea
CMake works out the build order itself: it scans every file for imports during the build, then compiles each module before its importers. That needs the Ninja or Visual Studio generator and a compiler it can scan with.
Checkpoint
Check: Modules in CMake
Practise: turn a header library into a module
This small library stores amounts in paise and prints rupees with Indian digit grouping, the last three digits first and then pairs, so ten million rupees prints as 1,00,00,000:
C header · money_header/money.h
// A small header library: amounts in paise, printed with Indian digit grouping.
#pragma once
#include <string>
namespace money {
struct Amount {
long long paise = 0;
};
Amount from_rupees(long long rupees, int paise = 0);
std::string to_text(Amount a); // "₹12,34,567.50"
std::string group_indian(long long rupees); // "12,34,567": the last three digits, then pairs
} // namespace money C++ (money.cpp) · money_header/money.cpp
#include "money.h"
namespace money {
Amount from_rupees(long long rupees, int paise) { return Amount{rupees * 100 + paise}; }
std::string group_indian(long long rupees) {
std::string digits = std::to_string(rupees);
if (digits.size() <= 3) return digits;
std::string out = digits.substr(digits.size() - 3);
for (std::size_t end = digits.size() - 3; end > 0;) {
std::size_t start = end >= 2 ? end - 2 : 0;
out = digits.substr(start, end - start) + "," + out;
end = start;
}
return out;
}
std::string to_text(Amount a) {
std::string paise = std::to_string(a.paise % 100);
if (paise.size() == 1) paise = "0" + paise;
return "₹" + group_indian(a.paise / 100) + "." + paise;
}
} // namespace money C++ (main.cpp) · money_header/main.cpp
#include <print>
#include "money.h"
int main() {
money::Amount fees = money::from_rupees(1234567, 50);
std::println("fees: {}", money::to_text(fees));
std::println("grouped: {} and {}", money::group_indian(999), money::group_indian(10000000));
} Turn it into a module called money with one partition, money:format, that holds group_indian. Keep the program’s
output exactly the same. Check four things: main.cpp imports money and includes no header of the library; the
partition is exported by the primary interface; the module’s own files include <string> only in their global module
fragments; and the build works with both compilers, the partition compiled first. Here is one solution, and its build:
CPPM (money-format.cppm) · money_module/money-format.cppm
// The format partition: Indian digit grouping.
module;
#include <string>
export module money:format;
export namespace money {
std::string group_indian(long long rupees) {
std::string digits = std::to_string(rupees);
if (digits.size() <= 3) return digits;
std::string out = digits.substr(digits.size() - 3);
for (std::size_t end = digits.size() - 3; end > 0;) {
std::size_t start = end >= 2 ? end - 2 : 0;
out = digits.substr(start, end - start) + "," + out;
end = start;
}
return out;
}
} // namespace money CPPM (money.cppm) · money_module/money.cppm
// The primary interface of money: amounts, and the format partition passed on to importers.
module;
#include <string>
export module money;
export import :format;
export namespace money {
struct Amount {
long long paise = 0;
};
Amount from_rupees(long long rupees, int paise = 0) { return Amount{rupees * 100 + paise}; }
std::string to_text(Amount a) {
std::string paise = std::to_string(a.paise % 100);
if (paise.size() == 1) paise = "0" + paise;
return "₹" + group_indian(a.paise / 100) + "." + paise;
}
} // namespace money C++ · money_module/main.cpp
#include <print>
#include <string>
import money;
int main() {
money::Amount fees = money::from_rupees(1234567, 50);
std::println("fees: {}", money::to_text(fees));
std::println("grouped: {} and {}", money::group_indian(999), money::group_indian(10000000));
} Text · money_module/CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(fees LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(money)
target_sources(money PUBLIC FILE_SET CXX_MODULES FILES money.cppm money-format.cppm)
add_executable(fees main.cpp)
target_link_libraries(fees PRIVATE money) from session import run
run("""
# The header library, as given.
clang++ -std=c++23 money_header/money.cpp money_header/main.cpp -o fees-header
./fees-header
# The module: the partition first, then the primary interface, then the importer.
clang++ -std=c++23 -fcxx-modules -c money_module/money-format.cppm -fmodule-output=money-format.pcm -o money-format.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c money_module/money.cppm -fmodule-output=money.pcm -o money.o
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. money_module/main.cpp money-format.o money.o -o fees
./fees
g++-16 -std=c++23 -fmodules money_module/money-format.cppm money_module/money.cppm money_module/main.cpp -o fees-gcc
./fees-gcc
""") Output
# The header library, as given. $ clang++ -std=c++23 money_header/money.cpp money_header/main.cpp -o fees-header $ ./fees-header fees: ₹12,34,567.50 grouped: 999 and 1,00,00,000 # The module: the partition first, then the primary interface, then the importer. $ clang++ -std=c++23 -fcxx-modules -c money_module/money-format.cppm -fmodule-output=money-format.pcm -o money-format.o $ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c money_module/money.cppm -fmodule-output=money.pcm -o money.o $ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. money_module/main.cpp money-format.o money.o -o fees $ ./fees fees: ₹12,34,567.50 grouped: 999 and 1,00,00,000 $ g++-16 -std=c++23 -fmodules money_module/money-format.cppm money_module/money.cppm money_module/main.cpp -o fees-gcc $ ./fees-gcc fees: ₹12,34,567.50 grouped: 999 and 1,00,00,000
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 money_session.py
GCC compiles all three files in one command because it handles them in the order they are given: list the partition first. With Clang, each interface writes its module file before the next file can read it.
Lakh Crore Converter (to Million, Billion) Check what group_indian should print for your own amounts: the converter shows a number with both the Indian and the international digit grouping.Then test the seven ideas of this lesson against the geometry and greeting files: every choice you make is explained on the spot, and the ideas you have shown least come up first.
Practice that adapts to you
Practise this lesson
13 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What does export do in a C++ module, and what does an importer not get?
In a module interface unit, export marks the declarations that importers can see: a function, a type, a whole
namespace block or a re-exported import such as export import :shapes;. Everything else declared in the module has
module linkage: the module’s own files may use it, and an importer that names it gets an error. An importer never gets
the module’s macros, because a named module carries no preprocessor state, and the module never sees the importer’s
macros either. An importer also cannot import the module’s partitions; it imports the module, and the primary
interface decides what is passed on. Finally, import only makes the module’s exported declarations available: it
does not compile the module, so the module file must already exist.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your team turns a widely used utility header into a module. Afterwards, files that used the header's MAX_RETRIES macro or its detail::parse helper stop compiling, and the CI job on Macs fails at CMake's generate step. Explain each failure and how you would fix it without reverting the change.
-
Mid to senior
A hand-written Makefile builds a library with three module interface units and a dozen importers. With make -j8 the build fails about one time in ten with "module not found", and always passes with make -j1. Explain the failure and how you would make the build correct.
Key takeaways
- An
#includepastes a header’s text into every file that includes it, while a module is compiled once into a module file that importers read, so the module has to be built before them. export module name;starts the primary interface,exportmarks what importers can use, everything else has module linkage, and implementation units (module name;) hold bodies that can change without rebuilding importers.- Partitions (
export module name:part;) split a module into files that only the module can import, and the primary interface must export every interface partition. - A module is compiled before its importers: Apple clang 21 needs
-fcxx-modules, GCC 16 needs-fmodules, and neither compiler’s feature-test macro is a reliable sign that modules work. - A module’s
#includelines belong in its global module fragment, betweenmodule;and the module declaration, so that what the headers declare stays in the global module that every other file uses. - CMake builds modules from a
CXX_MODULESfile set by scanning every file for imports first, with Ninja or Visual Studio, and stops at the generate step with Apple clang or a Makefile generator.
Terms in this lesson
- dependency scan
- A build step in which the compiler reads a source file only to report which modules it provides and imports. CMake runs one for every source of a C++20 target, then orders the compiles so that each module is built before its importers.
- feature-test macro
- A macro such as __cpp_modules or __cpp_lib_modules whose value, a year and month such as 202207, says that a compiler or its library claims a feature. A toolchain may build a feature without defining its macro, or define the macro without the feature.
- global module fragment
- The start of a module unit between module; and the module declaration. Only preprocessor directives such as #include may appear in it, and what they declare belongs to the global module, the module of every ordinary file.
- module file (BMI)
- The compiled form of a module interface that importers read instead of source text, such as geometry.pcm from Clang or gcm.cache/geometry.gcm from GCC. Clang's documentation calls it a built module interface, or BMI. It must exist before any importer is compiled.
- module implementation unit
- A file of a module that starts with module name; without export. It holds definitions, implicitly imports the primary interface and cannot be imported, so changing it does not change what importers read.
- module interface unit
- A file that starts with export module, as in export module geometry; or export module geometry:shapes;. The primary interface, the one without a partition name, exists once per module and decides what importers can use.
- module linkage
- What a name declared in a named module without export has: every file of that module may use it, and no file outside the module can.
- module partition
- A file of a module with a name after a colon, such as geometry:shapes. Only files of the same module can import it, with import :shapes;, and the primary interface must export every partition that is an interface unit.
- named module
- A part of a program declared with export module name; and brought into other files with import name;. It is compiled once into a module file, and importers get only the declarations it exports: never its other names and never its macros.
References
- Working Draft, Programming Languages: C++ (N4950), 10 Modules (ISO/IEC JTC1/SC22/WG21)
- P1103R3: Merging Modules (ISO/IEC JTC1/SC22/WG21)
- Standard C++ Modules (Clang documentation) (LLVM Project)
- C++ Modules (Using the GNU Compiler Collection) (GNU Project)
- GCC 16 Release Series: Changes, New Features, and Fixes (GNU Project)
- cmake-cxxmodules(7): C++ modules support in CMake (Kitware)
- target_sources (CMake documentation) (Kitware)
- Overview of modules in C++ (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress