Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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

  1. Question 1 of 2 A file says import geometry;. What does the compiler read for that line?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The module file that compiling the interface of geometry wrote

    import geometry; makes the compiler read the module file of geometry, the binary form of its interface that the compile of geometry.cppm wrote. That is why a module is compiled once, and why it must exist before any file that imports it is compiled.

  2. Question 2 of 2 A hand-written build script compiles geometry.cppm and main.cpp, which imports geometry, at the same time. What can go wrong?

    Choose one answer.

    A hint

    Think about what main.cpp needs to exist before its compile can start.

    Show the answer: Question 2 of 2

    Answer: main.cpp can start before the module file exists and stop with an error

    The last row of the table is the cost of modules: a module is compiled before every file that imports it. A build that runs the two compiles side by side works only when the module happens to finish first.

export decides what an importer can use

The geometry module has four files. Look at all of them before reading the explanation below:

The geometry module and a program that imports it

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:

Interactive · step by step Which file of the geometry program can use which name Step through the four files. An arrow is an import, and the chip on it is what travels along that import.
Which file of the geometry program can use which name, step 7 of 7. Showing geometry-shapes.cppm, geometry.cppm (exports area, perimeter), geometry.cpp, main.cpp (uses area, Rect, Circle). In focus: geometry.cpp.export import :shapesimplicit importimport geometrygeometry-shapes.cppmpartition geometry:shapesgeometry.cppmexports area, perimetergeometry.cppimplementation unitmain.cppuses area, Rect, Circle

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The primary interface. export module geometry; makes geometry.cppm the one file that says what the module offers. It declares area and perimeter inside export namespace geometry, and pi and square in a block without export.
  2. The partition passes types on. The partition geometry:shapes exports the types Rect and Circle. export import :shapes; imports the partition into the primary interface and exports what it exported.
  3. The module's own files see all. geometry.cpp starts with module geometry;, so it imports the primary interface without saying so and sees every name in it, exported or not: its bodies use pi and square.
  4. An importer sees the exports. main.cpp imports geometry and gets what is exported: area, perimeter, Rect and Circle. It printed plot: 100.00 sq m, fence 41.00 m with both compilers.
  5. Module linkage stops here. pi and square have module linkage: they never reach a file outside the module. Clang says of geometry::square in such a file that its declaration must be imported from module geometry before it is required.
  6. A macro stays in its file. GEOMETRY_VERSION is a macro of geometry.cppm, and no import of a named module carries a macro: neither main.cpp nor the module's own geometry.cpp can use it.
  7. Bodies change, importers stay. A changed function body in geometry.cpp recompiles only that file. The interface, and so the module file that main.cpp reads, stay as they were, so main.cpp is 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

  1. Question 1 of 2 Which of these can main.cpp use after import geometry;?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • geometry::Rect, declared in the partition geometry:shapes
    • geometry::area

    area is exported by the primary interface, and Rect is exported by the partition, which the primary interface passes on with export import :shapes. square and pi are declared without export, so they have module linkage and only the module's own files may use them. GEOMETRY_VERSION is a macro, and macros never cross an import.

  2. Question 2 of 2 geometry.cpp starts with module geometry;. What does that line make the file?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An implementation unit of geometry, which implicitly imports the primary interface and may use its names that are not exported

    A module declaration without export and without a partition name starts a module implementation unit. The standard says it implicitly imports the primary interface, so it sees every declaration of the interface, exported or not, such as square and pi. It cannot be imported, and changing it does not change the module file.

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:

  1. 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 imports geometry and nothing else.
  2. Every interface partition must be exported by the primary interface, directly or through another partition. That is what export import :shapes; does: Rect and Circle reach every importer of geometry. The standard requires no diagnostic when you forget it, so the mistake can show up as a confusing error far away.
  3. A partition without export, such as module 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 builds in order: the shapes partition, the primary interface, then geometry.cpp and main.cpp, then the link.1 geometry-shapes.cppmexport module geometry:shapes;module file of geometry:shapesgeometry-shapes.pcm (Clang)gcm.cache/geometry-shapes.gcm (GCC)2 geometry.cppmexport module geometry;export import :shapes;module file of geometrygeometry.pcm (Clang)gcm.cache/geometry.gcm (GCC)3 geometry.cppmodule geometry;3 main.cppimport geometry;4 linkerjoins the four object filesplotexecutablewritesread bywritesread byread byobject fileobject fileobject fileobject file

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. One question You delete export import :shapes; from geometry.cppm but keep the partition in the build. What does the standard say about the program?

    Choose one answer.

    Show the answer: One question

    Answer: It breaks a rule that needs no diagnostic: every interface partition must be exported by the primary interface

    The standard requires every module partition that is an interface unit to be exported by the primary interface, directly or indirectly, and requires no diagnostic when it is not. So the build may still succeed, with errors appearing later and elsewhere. A file outside the module can never import a partition.

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:

Building geometry and plot with both compilers Python · build_session.py
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:

Interactive · stage by stage The geometry build in the order of its imports Step through the Clang half of the session, one stage per command, then the GCC half, which keeps the same order.
The geometry build in the order of its imports, step 6 of 6. Showing Compile the partition (writes geometry-shapes.gcm), Compile the interface (writes geometry.gcm), Compile the importers (both read geometry.pcm), Link, Run plot (plot: 100.00 sq m). In focus: Compile the partition, Compile the interface.Compile the partitionwrites geometry-shapes.gcmCompile the interfacewrites geometry.gcmCompile the importersboth read geometry.pcmLinkfour object filesRun plotplot: 100.00 sq m

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The partition first. The partition imports nothing, so it can be compiled first. -fmodule-output=geometry-shapes.pcm makes Clang write its module file beside the object file.
  2. Then the interface. The primary interface imports the partition, so it can compile only now. -fprebuilt-module-path=. lets Clang find geometry-shapes.pcm, the colon of geometry:shapes written as a hyphen, and the interface writes geometry.pcm.
  3. Then every importer. geometry.cpp and main.cpp both read geometry.pcm and nothing else of the module. Neither imports the other, so a build may compile them at the same time.
  4. 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.
  5. The run. plot printed plot: 100.00 sq m, fence 41.00 m and well: 4.52 sq m, and ls -1 *.pcm listed the two module files, geometry-shapes.pcm and geometry.pcm.
  6. GCC keeps the order. GCC 16 needs -fmodules and no option for module files: it wrote geometry-shapes.gcm and geometry.gcm into gcm.cache and found them there, in the same order, and its plot printed the same two lines.

What each flag does:

  • -fcxx-modules (Apple clang). Apple’s compiler reads export module as 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 expects geometry.pcm for the module and geometry-shapes.pcm for the partition geometry: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 called gcm.cache and 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.

features.cpp C++ · features.cpp
// 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
}
The two feature-test macros, as each compiler sets them Python · features_session.py
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

  1. Question 1 of 2 You build geometry by hand with Clang. Which compile has to come first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: geometry-shapes.cppm, which imports nothing and writes geometry-shapes.pcm

    A file compiles only after every module it imports: the partition, then the primary interface, then geometry.cpp and main.cpp, which do not depend on each other and can be compiled at the same time.

  2. Question 2 of 2 A configure step must decide whether the compiler can build modules. Which test is dependable?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Compile a small module and a file that imports it, and see whether that works

    The feature-test macros say what a compiler claims, and the two compilers of this lesson claim less, or other things, than they can do. A configure step that compiles a small module tests the toolchain itself.

Five mistakes stop most first builds

Each command below makes one of the classic mistakes:

Five mistakes and what each compiler says Python · mistakes_session.py
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:

  1. The modules flag is missing. Apple clang reports unknown type name 'module', because without -fcxx-modules the word is an ordinary name. GCC is kinder: its note says that C++20 module is only available with -fmodules.
  2. 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.
  3. A name that is not exported. Clang’s message is the precise one: the declaration of square exists, but it is not visible outside the module. GCC simply reports that square is not a member of geometry.
  4. 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 constexpr variable instead.
  5. 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

  1. Question 1 of 2 GCC stops with "failed to read compiled module: No such file or directory" and the note "compiled module file is gcm.cache/geometry.gcm". Which mistake caused it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A file that imports geometry was compiled before the interface of geometry

    The message is GCC's way of saying the second mistake: the importer came first, so gcm.cache/geometry.gcm did not exist yet. Compile in the order of the import graph, or let a build system work it out.

  2. Question 2 of 2 Importers of geometry need its version number, today the macro GEOMETRY_VERSION in geometry.cppm. Which change gives it to them?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Export a constexpr variable that holds the value, such as geometry::version

    Macros never cross an import, in either direction. A value that importers need becomes an exported constant, as the reading of the fourth message says.

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:

A module that needs a header, and the same module done wrong

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:

The fragment done right, then the include in the wrong place Python · gmf_session.py
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:

Interactive · step by step Where the declarations of a module unit belong Step through greeting.cppm line by line, then the same
Where the declarations of a module unit belong, step 7 of 7. Showing module;, #include <string>, export module greeting;, greet, #include <string>, hello.cpp, global module, module greeting. In focus: module greeting.belongs toattached tothe same std::stringattached tomodule;opens the fragment#include <string>in the fragmentexport module greeting;starts the purviewgreetexported, returns a string#include <string>in greeting_wrong.cppmhello.cppincludes <string>global moduleevery ordinary file toomodule greetinga named moduleerror: declared in two modules

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The fragment opens. module; on the first line of greeting.cppm opens the global module fragment. Only preprocessor directives, such as #include, may appear in it.
  2. A header in the fragment. #include <string> pastes the header here, and everything it declares, std::string included, belongs to the global module: the module of every ordinary file.
  3. 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 to greeting.
  4. greet belongs to greeting. greet is declared in the purview and exported, so it is attached to greeting. The std::string it returns is the global one, from the fragment.
  5. The importer agrees. hello.cpp includes <string> itself and gets the same std::string from the global module, so the two files agree on the type. Both compilers built it, and it printed Namaste, Kavya!.
  6. The include below the line. greeting_wrong.cppm has the same #include below export module greeting;. Everything the header declares would then be attached to greeting: a second, private copy of the standard library.
  7. The two copies clash. Clang warned that the #include attaches the declarations to greeting, then failed because tm was declared in both modules; GCC reported a conflicting declaration in module greeting.

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

  1. Question 1 of 2 What is wrong with this module unit?

    Read the code, then choose one answer.

    export module report;
    
    #include <vector>
    
    export std::vector<int> totals();
    Show the answer: Question 1 of 2

    Answer: The include comes after the module declaration, so what vector declares is attached to the module report instead of the global module

    Headers belong in the global module fragment: module; at the top, then the #include lines, then export module report;. An #include in the module's purview attaches what the header declares to the module, which clashes with the same declarations in the global module. Clang warns about exactly this, and both compilers then fail.

  2. Question 2 of 2 What may appear between module; and export module greeting;?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only preprocessor directives, such as #include and #define

    module; opens the global module fragment and the module declaration closes it. It holds only preprocessor directives, and what the included headers declare belongs to the global module, the module of every ordinary file.

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:

CMakeLists.txt for the geometry library and plot Text · geometry/CMakeLists.txt
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:

The geometry project with GCC 16, LLVM 23, Apple clang and Make Python · cmake_session.py
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:

Interactive · step by step What CMake and Ninja did with the geometry project Step through the GCC 16 build, one stage of Ninja's log at a time, then the two configures that stopped.
What CMake and Ninja did with the geometry project, step 9 of 9. Showing Configure, Generate, Scan for imports (steps 1 to 3 and 5), Collate (steps 4 and 9), Compile in order (steps 6 to 8), Archive and link (steps 10 to 12), Run plot (plot: 100.00 sq m), Apple clang 21, Unix Makefiles. In focus: Unix Makefiles.no import scannerno module supportConfigurefound GNU 16.2.0Generatewrites build.ninjaScan for importssteps 1 to 3 and 5Collatesteps 4 and 9Compile in ordersteps 6 to 8Archive and linksteps 10 to 12Run plotplot: 100.00 sq mApple clang 21stops at generateUnix Makefilesstops at generate

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. Configure. cmake -S . -B build-gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16 found GNU 16.2.0. The CXX_MODULES file set told CMake which two files are module interfaces.
  2. 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.
  3. Scan. Ninja's steps [1/12] to [3/12] asked GCC which modules the three files of geometry provide and import, and [5/12] asked the same about main.cpp. Nothing was compiled yet.
  4. Collate. [4/12] collated the scans of geometry into CXX.dd, the file that tells Ninja which compile needs which module file, and [9/12] wrote the one of plot.
  5. 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, although CMakeLists.txt lists geometry.cppm before the partition.
  6. Archive and link. [10/12] archived libgeometry.a, [11/12] compiled main.cpp, which needed the module file of geometry, and [12/12] linked plot.
  7. Run. plot printed plot: 100.00 sq m, fence 41.00 m and well: 4.52 sq m, as the hand build did. Homebrew's LLVM 23 built the same project in 12 steps too.
  8. 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-deps found no scanner in the Command Line Tools either.
  9. 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

  1. Question 1 of 2 Which combination builds the geometry module with CMake 4.4 on a Mac?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The Ninja generator with GCC 16 as the compiler

    CMake's module support needs a generator that handles the scanned dependencies (Ninja 1.11 or newer, or Visual Studio 2022 or newer) and a compiler that can report a file's imports: Clang 16 or newer, GCC 14 or newer or MSVC 14.34 or newer. Apple clang cannot, so CMake stops at the generate step; the Makefile generators do not support modules at all.

  2. Question 2 of 2 CMakeLists.txt lists geometry.cppm before geometry-shapes.cppm, yet Ninja compiled the partition first. What decided the order?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The scans: CMake collated which file provides and imports which module into CXX.dd, and Ninja followed it

    During the build, CMake asks the compiler which modules each file provides and imports, collates the answers into one CXX.dd file per target, and Ninja then runs the compiles in that order: partition, primary interface, implementation unit, and main.cpp after the library.

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:

The money library, header version

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:

One solution: a module with a format partition

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)
The header library and the module, built and run Python · money_session.py
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.

  1. Question 1 of 13 What does a file that imports a module never receive from it?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: The macros defined in the files of the module

    An import carries the exported declarations of the module and nothing else: no names with module linkage and no macros. That is what keeps the macros of one library away from the files of another.

  2. Question 2 of 13 Two libraries both define a macro called min in their headers, and every file that includes both breaks. Both libraries become modules. Why does the clash go away?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: Macros never leave a module, so neither min reaches the files that import them

    The clash came from text: both headers pasted a min macro into every file that included them. Modules export declarations only, so the macros stay inside each library and the files that import both see neither.

  3. Question 3 of 13 You change the body of geometry::area in geometry.cpp. Which files have to be compiled again before the program is linked?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: Only geometry.cpp

    An implementation unit holds definitions that importers never read. Changing one recompiles that file alone, and the link then picks up its new object file.

  4. Question 4 of 13 How does the primary interface geometry.cppm bring in its partition and pass its exports on?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: export import :shapes;

    Within a module, a partition is named by its short form, :shapes. Importing it with export import makes its exports part of the module's interface, as the second partition rule requires.

  5. Question 5 of 13 The files of geometry share helper code that importers must never see, and you want it in a file of its own. What should that file be?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: An internal partition, module geometry:detail;, which the module's files import with import :detail;

    Internal partitions are the third kind of partition: module units that share declarations among the files of one module and stay out of its interface.

  6. Question 6 of 13 Put the steps of the geometry build in an order that works.

    Give each item its position, from 1 (first).

    Show the answer: Question 6 of 13

    Answer:

    1. Compile geometry-shapes.cppm, which writes the partition's module file
    2. Compile geometry.cppm, which imports the partition and writes the module file of geometry
    3. Compile geometry.cpp and main.cpp, which both read the module file of geometry
    4. Link the four object files into plot

    A file can be compiled only after every module it imports, so the build follows the import graph: the partition first, then the primary interface that imports it, then the files that import the module. The two files of step 3 do not depend on each other and can be compiled in parallel.

  7. Question 7 of 13 Which Clang option says where to look for the module files a compile needs?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: -fprebuilt-module-path=.

    Clang writes a module file with -fmodule-output and finds module files with -fprebuilt-module-path, where it expects geometry.pcm for the module and geometry-shapes.pcm for the partition geometry:shapes.

  8. Question 8 of 13 Apple clang reports unknown type name 'module' on the line export module geometry:shapes;. What is missing?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: The -fcxx-modules flag

    The first mistake of the session: Apple clang treats export module as an error unless -fcxx-modules is given. GCC's message for the same mistake says outright that module is only available with -fmodules.

  9. Question 9 of 13 main.cpp starts with import geometry:shapes; and both compilers stop at the colon. What is the fix?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: Import the module with import geometry;, which passes the types of the partition on

    A partition belongs to its module. Files outside it import the module, and the primary interface decides what the partitions pass on.

  10. Question 10 of 13 greeting.cppm includes <string> in its global module fragment; hello.cpp includes <string> and imports greeting. Is the std::string that greet returns the type hello.cpp uses?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: Yes: both come from the global module, so they are one type

    The global module fragment exists for this: what a module's headers declare stays in the global module, so the module and its importers agree on every standard type.

  11. Question 11 of 13 How does CMake learn that geometry.cppm and geometry-shapes.cppm are module interfaces?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: They are listed in a FILE_SET of type CXX_MODULES in target_sources

    target_sources(geometry PUBLIC FILE_SET CXX_MODULES FILES geometry.cppm geometry-shapes.cppm) marks the interfaces; geometry.cpp, the implementation unit, is an ordinary PRIVATE source.

  12. Question 12 of 13 Where does geometry.cpp, the implementation unit, go in target_sources?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: With the PRIVATE sources, as an ordinary file

    Interfaces go in the CXX_MODULES file set; implementation units and other files are ordinary sources, which a C++20 target scans all the same.

  13. Question 13 of 13 The Mac job of your CI configures geometry with Apple clang and stops at the generate step. Which change makes it build?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Configure with GCC 16 or Homebrew's LLVM as the compiler, with Ninja

    CMake needs a compiler it can scan with and a generator that handles the scanned dependencies. On a Mac that means GCC 16 or upstream Clang, with Ninja.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An #include pastes 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, export marks 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 #include lines belong in its global module fragment, between module; 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_MODULES file 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.