Visual walkthrough — Modules (C++20) — concept and syntax
We build everything from zero. No word is used before it has a picture.
Step 1 — The old world: #include is a photocopier
WHAT. We have two source files, a.cpp and b.cpp, both writing #include "math.h".
WHY show this first? To feel the pain the module system was invented to cure. If you do not see the waste, import looks like pointless new syntax.
PICTURE. Look at the figure. One header file math.h (the mint page) gets copied whole into both files (the two lavender pages). The same text is parsed twice — once per .cpp. Multiply "twice" by hundreds of files including hundreds of headers and you get the slow builds the parent note complained about.

Step 2 — What "parsing" costs, and why repetition hurts
WHAT. We count how many times the same header text is parsed.
WHY this tool — counting? Because "modules are faster" is a quantitative claim. To justify it we must literally count re-parses. This is the Build Systems and Compilation Speed argument in one number.
PICTURE. The bars show parse-work. With #include (coral bars) the header's parse cost appears once per including file — three files, three full parses. With import (mint bar) the header is parsed once, compiled to a stored form, and every importer just reads that stored form.

Step 3 — Build the module: export module math;
WHAT. We write the primary interface unit.
// math.ixx — primary interface unit
export module math; // <- names this box "math", marks it the front coverTerm by term:
export— "outsiders are allowed to know about this box."module— the keyword that says box, not ordinary file.math— the box's name; importers will later typeimport math;.
WHY this step / why this keyword? We need a single unambiguous name so the compiler can store one compiled interface and later hand it to any importer. Ordinary files have no such handle; that is why old headers needed textual copying. A name replaces the copy.
PICTURE. The lavender box has a labelled front cover (export module math;). Nothing has crossed out of it yet — the box is sealed. See how this differs from Namespaces in C++: a namespace only groups names; a module seals a compilation boundary.

Step 4 — Two kinds of names inside the box
WHAT. We add two functions.
export module math;
export int add(int a, int b) { return a + b; } // gate OPEN: crosses out
int secret_helper() { return 42; } // gate CLOSED: stays insideTerm by term on the exported line:
export— the gate token; put it in front of a declaration and that name may leave the box.int add(int a, int b)— the name being exported: a function returningint.- (no
export) onsecret_helper— no token, no gate, no exit.
WHY. This is encapsulation at file level (parent note's phrase). The box decides its own public surface. #include could never do this — a header exposes everything it contains.
PICTURE. The green arrow (add) passes through the gate in the box wall; the coral name (secret_helper) bounces off the inside wall. One picture, the whole meaning of export.

Step 5 — Compile the box into a sealed interface
WHAT. math.ixx → compiler → math's compiled interface (holds only add). Read → as "is compiled into", exactly as just defined.
WHY this matters. This artifact is the whole reason back in Step 2. Reading it is fast because the hard work (parsing, checking) already happened once. secret_helper is not in it — the seal was applied at the gate.
PICTURE. The lavender source box flows through a compiler gear into a small butter-coloured "sealed interface" tag. Notice secret_helper was left behind on the source side; only add made it onto the tag.

Step 6 — Import: read the seal, no photocopy
WHAT. Another file uses the module.
// main.cpp
import math; // read math's sealed interface
#include <iostream>
int main() {
std::cout << add(2, 3); // OK: add was exported -> prints 5
// secret_helper(); // ERROR: never crossed the gate
}Term by term:
import math;— request the box namedmath; receive its exported names (add).add(2, 3)— legal,addis on the seal; the result is .secret_helper()— illegal, it is invisible here; the compiler has never heard of it.
WHY. Contrast Step 1: there the whole header text arrived. Here only the gate-approved surface arrives, already compiled. No macro leakage, no order dependence — because there is no text being pasted at all.
PICTURE. The sealed tag from Step 5 slides into main.cpp; only add becomes visible (green), secret_helper shown greyed-out and unreachable behind the box wall.

Step 7 — Edge case: #include after export module (the quarantine), and header units
WHAT. Where #include is allowed vs forbidden, and the modern alternative.
module; // <- global module fragment BEGINS
#include <cmath> // legal ONLY here (quarantine zone)
export module geometry; // named-module region begins; no #include past this line
export double hypot2(double a, double b) { return std::sqrt(a*a + b*b); }WHY a whole step for this? Because it is the single most common beginner error (parent note's first mistake). The rule is positional, so it needs a picture, not a sentence. Legacy #include machinery is the The C++ Preprocessor; it is quarantined so its macros cannot leak into the sealed part.
The header-unit alternative (C++20). Instead of pasting a header's text you can import the header as a module-like unit:
export module geometry;
import <cmath>; // header UNIT: <cmath> compiled once, then reusedimport <cmath>;— treat the standard header<cmath>as a header unit: parsed once, cached, then read cheaply (like a real module).- Unlike
import math;, a header unit does carry the header's macros across — that is the deliberate escape hatch for macro-dependent legacy headers. - Because it is an
import(not#include), it is legal inside the named-module region — it does not need the quarantine. Only textual#includeis banned pastexport module.
PICTURE. A red vertical line marks export module geometry;. Left of it (mint zone) #include is allowed. Right of it (lavender zone) #include is forbidden — a red ✗ — but import <cmath>; (butter arrow) is welcome, because it is an import, not a paste.

Step 8 — The four regions of one module file (the private module fragment)
WHAT. The complete layout of a primary interface unit.
module; // (1) global module fragment — #include lives here
#include <cmath>
export module geometry; // (2) named-module region begins
export double area(double r); // (3) purpose region — exported + private declarations
double helper(double x); // (private: no export)
module : private; // (4) PRIVATE module fragment begins
double helper(double x) // definitions here are invisible to importers
{ return x * x; } // AND changing them never forces importers to rebuildRegion by region:
(1)global module fragment —module;… up toexport module. Only place textual#includemay sit.(2)named-module region — starts atexport module geometry;. From here on,#includeis banned (use header units).(3)purpose region — your exported and private declarations; the substance of the module.(4)module : private;opens the private module fragment: a tail zone whose contents are completely invisible outside the module. Its special power: editing code here does not invalidate importers, so their builds are not triggered.
WHY a whole step? Without it the decomposition model has a missing quadrant. module : private; is not the same as an unexported declaration in region (3): both are private to importers, but only the private fragment guarantees importers won't rebuild when it changes.
PICTURE. Four stacked bands in a single file, top to bottom, each labelled with its rule; a red ✗ #include tag sits on every band after the first.

Step 9 — Degenerate case: one cover, many pages, and partitions
WHAT. The legal shapes, and the illegal one.
| Line at top of file | Role | How many allowed |
|---|---|---|
export module math; |
primary interface (front cover) | exactly one |
module math; |
implementation unit (inside page) | many |
export module math:geom; |
interface partition (named sub-section) | many |
a second export module math; |
— | illegal |
WHY show the illegal one? Covering all cases means naming what breaks. Two front covers = two definitions of the box's public face = ambiguity. The compiler rejects it.
PICTURE. One butter cover on top; several lavender inside pages beneath it; a mint "partition" tab clipped to the side (same box, sub-labelled :geom); a second cover drawn faded with a red ✗.

The one-picture summary
The whole journey on one canvas: a source box (export module math;) with a gate that lets add out and keeps secret_helper in → compiled once into a sealed interface → imported by any number of files, each reading the seal cheaply instead of re-parsing text. On the left, the quarantine room confines legacy textual #include; a header unit (import <cmath>;) bypasses it entirely. (Every → here means "is compiled into", per Step 5.)

Recall Feynman retelling — say it in plain words
Imagine a factory that makes one perfect labelled crate. Inside the crate are parts; a gate in the crate wall decides which parts are shown on the shipping label (those are exported) and which stay bolted inside forever (those are private). The factory builds and inspects this crate exactly once, then prints a small sealed label listing only the shown parts.
Anyone who wants the parts doesn't get a photocopy of the whole factory (that was the old #include way — slow, and it dragged in every stray note pinned to the walls, i.e. macros). Instead they just read the tiny label: import math;. They can use add, because add is on the label; they cannot use secret_helper, because it never left the crate.
Old header machinery still exists but is locked in one room at the front — the global module fragment, before export module Name;. You may keep textual #include there and nowhere past the door; if you'd rather, import <cmath>; treats the header as its own little cached crate and is allowed past the door (and it, uniquely, still carries macros). At the very back of the file there is a private room, module : private;, whose contents nobody outside ever sees and whose edits never force importers to rebuild.
Every crate has exactly one label (one primary interface); extra inside pages (module math;) add bolts but no new label entries; extra labelled sub-sections are partitions (export module math:geom;). That is the entire idea, and every figure on this page is one frozen frame of that crate's life.
Recall Quick self-test
- What replaces the photocopied header text? → a compiled (binary) module interface, read once.
- Which names appear on the sealed interface? → only the
exported ones. - Where alone may textual
#includelive in a module? → the global module fragment, beforeexport module. - How can you use a legacy header past that border? → import it as a header unit, e.g.
import <cmath>;. - What does
module : private;add? → a private module fragment: invisible to importers, and editing it never rebuilds them. - How many primary interface units per module? → exactly one.
Prerequisites revisited: Translation Units and Linkage, The C++ Preprocessor, Namespaces in C++, Build Systems and Compilation Speed, Module Partitions. Parent: the Hinglish companion.