Intuition What this page is
The parent note taught the rules . Here we run those rules through every kind of situation a C++ module file can be in — every position of export, every legal and illegal spot for #include, single-file vs split, partitions, the standard library, and the traps exams love. Each example first asks you to forecast the answer, then walks the reasoning, then verifies it by tracing what the compiler sees.
Prerequisite ideas we lean on: The C++ Preprocessor (what #include really does), Translation Units and Linkage (what a "compiled unit" is), Namespaces in C++ (a different axis of visibility), Module Partitions (splitting one module across files), and Build Systems and Compilation Speed (why any of this matters).
Two phrases appear in every example below — global module fragment and named-module region — so let us pin them to a picture before we use them. The line that opens the fragment is the bare word module; — one token, a semicolon, nothing else — so define it before we lean on it.
module; marker
module; (the word module followed immediately by a semicolon, with no name ) is not a module declaration — it is the opening of the global module fragment . It says: "the lines that follow, up until export module Name;, are the airlock where legacy preprocessor code lives." In the figure it is the top plum bar.
Why is it required? Without a module; line there is no fragment at all , and then there is nowhere legal to put a #include — because #include is banned inside the named-module region (below the orange divider). So if you need even one legacy header, you must open with module;.
What if you omit it? Then your file must start straight at export module Name; and contain zero #include directives. Adding a #include without a preceding module; is a compile error (E3 shows this).
Careful: module; (bare) ≠ module Name; (which is an implementation unit , E4) ≠ export module Name; (the interface unit ). Three different lines, three different jobs.
Definition The two regions of a module file
Read a module file top-to-bottom. It has (at most) two zones, separated by the line export module Name;:
Global module fragment — the optional band between module; and export module Name;. This is a quarantine zone : the only place a legacy #include may live. Think of it as an airlock where textual paste from The C++ Preprocessor is contained.
Named-module region — everything from export module Name; onward. This is the clean, compiled world. No #include is allowed here. Names you write here belong to the module and can be exported.
Why quarantine #include at all? A #include is textual paste (see The C++ Preprocessor ): it dumps another file's raw text — and its macros — into yours before compilation. The standard requires this paste to happen before the named-module region opens so that the compiled module interface stays clean: names in the module are attached to the module, but the pasted header text belongs to the global module (the nameless bucket the fragment feeds), so it never becomes part of what importers see. Quarantine = "let the old messy paste happen, but wall it off from the new clean interface."
If you use no legacy headers, you skip the fragment entirely and start straight at export module Name; (the degenerate case in E1).
In the figure, follow the plum-shaded band (the fragment / airlock, opened by module;) and the teal region below the dividing line (the named module). Every later example is just a variation on where a line falls relative to that dividing line.
Here is the full space of cases a module topic can throw at you. Every cell below is covered by at least one worked example — the example numbers are in brackets.
Axis
Case class
Covered by
Export position
single export on one declaration
E1
export { ... } block (group)
E1
no export → private name
E1, E2
#include placement
legal: inside global module fragment
E3
illegal: after export module
E3
degenerate: no fragment needed at all
E1
File roles
one file does everything (interface = implementation)
E1
split: interface unit + implementation unit
E4
Boundary confusion
export vs class public/private
E5
Multiplicity
two files claim same export module (error)
E6
correct fix: partitions
E6
Macros
macro leakage: does import carry it? (no)
E7
header unit import "h.h" does carry it
E7
Standard library
import std; / header units vs #include
E7
Real-world word problem
build-speed estimate: parse-once payoff
E8
Exam twist
trace what compiles / what's visible where
E2, E5, E9
Worked example Which names can
main.cpp call?
// math.ixx (primary interface unit)
export module math;
export int add ( int a, int b) { return a + b; } // single export
export { // group export
int sub ( int a, int b) { return a - b; }
int mul ( int a, int b) { return a * b; }
}
int helper ( int x ) { return x * x; } // no export -> private
// main.cpp
import math;
int main () { return add ( 1 , 2 ) + sub ( 9 , 4 ) + mul ( 2 , 5 ); } // returns ?
Forecast: which four names exist in math, and which three are visible in main? What integer does main return?
Step 1 — list every name the module defines . add, sub, mul, helper.
Why this step? Definition and export are two separate questions; separate them first or you will conflate them.
Step 2 — mark which cross the boundary. add (single export), sub and mul (inside the export {} group). helper has no export → it stays private to the module. In the figure, the three exported names sit outside the module wall; helper is trapped inside .
Why this step? The export keyword is the gate; the group block is identical in meaning to writing export on each line — pure convenience.
Step 3 — check #include needs. None. No header is used, so there is no global module fragment — the degenerate, cleanest case. The file may start straight with export module math;, with no module; line at all.
Why this step? You never add a module; fragment "just in case" — you add it only when a legacy #include demands it.
Step 4 — compute the return. a dd ( 1 , 2 ) = 3 , s u b ( 9 , 4 ) = 5 , m u l ( 2 , 5 ) = 10 , total 3 + 5 + 10 = 18 .
Why this step? main only touches exported names, so it compiles; now arithmetic is trivial.
Verify: helper is never called from main, so no visibility error. Sum 3 + 5 + 10 = 18 . ✅
Worked example Does this compile?
Same math.ixx as E1. Now:
import math;
int main () { return helper ( 4 ); } // ?
Forecast: compiles or errors?
Step 1 — is helper exported? No — E1 established it has no export.
Why this step? Visibility across the module boundary is decided only by export, nothing else.
Step 2 — what does import math; bring? Exactly the exported surface : add, sub, mul. Not helper (recall it was trapped inside the module wall in the E1 figure).
Why this step? This is the whole selling point of modules — the private interior is invisible, no leakage.
Step 3 — conclusion. helper is an undeclared name in main.cpp → compile error ("helper was not declared in this scope").
Verify: If main had called an exported name like add(4,4), it would compile and return 8 . Only helper fails. ✅
Worked example Two versions — one compiles, one is rejected. Which?
Version A
module ; // global module fragment BEGINS
#include <cmath> // legacy header — allowed here
export module geometry; // named-module region begins now
export double hyp ( double a, double b) { return std :: sqrt (a * a + b * b); }
Version B
export module geometry;
#include <cmath> // <-- after the named module started
export double hyp ( double a, double b) { return std :: sqrt (a * a + b * b); }
Forecast: which one does the compiler accept?
Step 1 — locate the #include. In A it sits between module; and export module geometry; — the global module fragment , the plum-shaded airlock on the left of the figure. In B it sits below the export module geometry; divider — inside the teal named-module region.
Why this step? The single rule is positional: #include is only legal in the fragment (recall the anatomy diagram above).
Step 2 — apply the rule. A is legal (its module; line opened a fragment for the #include to live in). B violates the standard: no #include once the named module has begun — and B never opened a fragment because it has no module; line.
Why this step? A #include is textual paste from The C++ Preprocessor ; it must be quarantined before the module's clean, compiled region starts, or it would re-pollute it.
Step 3 — numeric sanity on hyp. With a = 3 , b = 4 : 3 2 + 4 2 = 9 + 16 = 25 = 5 .
Why this step? Confirms hyp computes the Euclidean length (a right-triangle hypotenuse) once the file compiles — a value check independent of the placement rule.
Verify: Version A: h y p ( 3 , 4 ) = 5 . Version B: rejected before it ever runs. ✅
Worked example Where does each definition live, and does
main see bal_?
// account.ixx (interface unit)
export module account;
export class Account {
public:
Account ( double b );
void deposit ( double a );
double balance () const ;
private:
double bal_;
};
// account.cpp (implementation unit — NO export)
module account; // note: bare 'module Name'
Account :: Account ( double b) : bal_ (b) {}
void Account :: deposit ( double a ) { bal_ += a; }
double Account :: balance () const { return bal_; }
// main.cpp
import account;
int main () {
Account acc ( 100.0 );
acc. deposit ( 25.0 );
return static_cast<int> (acc. balance ()); // ?
}
Forecast: what integer prints, and can main read acc.bal_ directly?
Step 1 — role of each file. account.ixx = the one primary interface unit (export module). account.cpp = implementation unit (module account; — named , no export; contrast with the bare, nameless module; fragment line from E3). It adds definitions but no new public names . The figure shows the interface file feeding the public face, the implementation file feeding only bodies.
Why this step? Keeping bodies out of the interface means changing a body recompiles only account.cpp, not every importer — the Build Systems and Compilation Speed payoff.
Step 2 — what main can call. Account's constructor, deposit, balance are public and the class is exported → visible. bal_ is private → blocked at the class level regardless of module status.
Why this step? Two axes: export decided the class crosses the module boundary; public/private decides member access after it crossed.
Step 3 — trace the balance. Start 100.0 ; deposit(25.0) → 100.0 + 25.0 = 125.0 ; cast to int → 125 .
Why this step? Sanity-check the actual number the program returns.
Verify: Return value = 125 . Direct acc.bal_ access → compile error (private member). ✅
Worked example Four combinations — which are usable from outside?
export module widget;
export class W { // class W crosses the module boundary
public: int a (); // (1) exported class + public
private: int b (); // (2) exported class + private
};
class Hidden { // NOT exported
public: int c (); // (3) private class + public member
};
Forecast: which of W::a, W::b, Hidden::c can an importer use?
Step 1 — the two independent axes. export = "does this file-level name leave the module?" public/private = "which members of a class may callers touch?" They multiply, they don't overlap. The figure draws them as two doors in series.
Why this step? The classic exam trap is treating export as a synonym for public. It is not — they answer different questions.
Step 2 — evaluate each cell.
(1) W::a — class exported ✅ and member public ✅ → usable .
(2) W::b — class exported ✅ but member private ✗ → not usable (blocked by class access).
(3) Hidden::c — class not exported ✗ → the whole class is invisible to importers, so c is unreachable → not usable .
Why this step? Both gates must be open. Fail either one and the name is unreachable.
Step 3 — count usable names. Exactly 1 of the three (W::a).
Why this step? Reducing the trace to a single count is the answer an exam actually grades — and it forces you to apply the AND-of-two-doors rule to every row rather than eyeballing one.
Verify: A truth-table view — usable ⇔ (class exported) AND (member public). Only row (1) is true AND true. ✅
A name reaches the outside only if it passes both doors: the module door (export) and the class door (public). Miss either → stuck inside.
Worked example Fix the "one module, two interface files" error.
// shape_a.ixx
export module shape; // primary interface unit #1
// shape_b.ixx
export module shape; // primary interface unit #2 <-- illegal
Forecast: what's the error, and what's the correct multi-file layout?
Step 1 — count primary interface units. A module has exactly one file that says export module Name; (no colon). Here two files do → the compiler/linker rejects a duplicate primary interface unit .
Why this step? The primary interface is the module's single "public face"; two faces would make import shape; ambiguous.
Step 2 — the correct pattern: partitions. Split the public surface into named partitions, each export module shape:name;, then re-export them from the one primary unit:
// shape-circle.ixx (interface PARTITION)
export module shape:circle;
export double area_circle ( double r);
// shape-rect.ixx (interface PARTITION)
export module shape:rect;
export double area_rect ( double w, double h);
// shape.ixx (THE single primary interface unit)
export module shape;
export import :circle; // pull partition into the public face
export import :rect;
Why this step? The colon : names a partition; export import :circle; re-exports it so importers of shape see everything. Exactly one colon-less export module shape; remains. See Module Partitions . The figure shows the two illegal peers on the left and the correct hub-and-partitions layout on the right.
Step 3 — numeric sanity. Give the partitions bodies a r e a _ c i r c l e ( r ) = π r 2 and a r e a _ r ec t ( w , h ) = w h . Then a r e a _ r ec t ( 3 , 4 ) = 12 and a r e a _ c i r c l e ( 2 ) = 4 π ≈ 12.566 .
Why this step? A layout can be legal yet still compute the wrong thing; checking the formulas confirms the partitioned code is not just structurally valid but numerically correct.
Verify: One colon-less unit → legal. a r e a _ r ec t ( 3 , 4 ) = 12 exactly; a r e a _ c i r c l e ( 2 ) = 4 π . ✅
MAX leak? And how does a header unit differ from #include and from a module import?
// config.h
#define MAX 999
// util.ixx
module ;
#include "config.h" // MAX exists inside the fragment
export module util;
export int cap ( int x) { return x > MAX ? MAX : x; } // fragment sees MAX
// main.cpp
import util; // (A) named-module import
int main () {
int y = cap ( 1500 ); // does this work?
int z = MAX; // (B) is MAX visible here?
return y; // ?
}
Forecast: value of y; is line (B) legal after import util;?
Step 1 — cap's use of MAX. MAX came from #include "config.h" inside util.ixx's global module fragment (opened by the module; line), so cap's body legitimately used it at compile time of util.ixx . That's fine.
Why this step? Macro substitution happens before compilation, entirely inside the fragment — nothing leaks out into a compiled interface.
Step 2 — does import util; carry MAX into main.cpp? No. A named-module import (import util;) deliberately exports no macros . So line (B) int z = MAX; → MAX undeclared → error . The figure shows the macro blocked at the module boundary.
Why this step? Killing macro leakage is a feature , not a bug — it fixes problem #2 from the parent note.
Step 3 — the three ways to pull in config.h, contrasted.
Mechanism
Syntax
Re-parsed each use?
Carries macros (MAX)?
Legacy include
#include "config.h"
Yes (textual paste, every TU)
Yes
Header unit
import "config.h";
No (parsed once, cached)
Yes
Named module
import util;
No
No
A header unit (import "config.h";) is the middle ground: it is imported like a module (parsed once, fast) but it does preserve the header's macros. So if main.cpp genuinely needed MAX, it would write import "config.h"; (a header unit), not import util;.
Why this step? This is exactly the case the matrix promises under "Macros" and "Standard library": a header unit gives you speed and macro visibility, unlike either extreme.
Step 4 — the standard library. The C++23 feature import std; exposes the whole standard library as a module (fast, no macro leakage). Some toolchains also let you import <iostream>; as a header unit . But note: importable standard-library headers and import std; are optional/implementation-defined — check your compiler before relying on them.
Why this step? Corrects the common myth that import <iostream>; is a portable C++20 given — it isn't.
Step 5 — compute y. cap(1500): since 1500 > 999 , return 999 . So y = 999.
Why this step? The macro discussion is about visibility ; we still owe the reader the actual runtime value, which depends on cap's clamping logic, not on where MAX came from.
Verify: y = 999 (clamped to MAX). Line (A) import util; compiles; line (B) int z = MAX; errors (named-module import exports no macros); switching to a header unit import "config.h"; would make MAX visible. ✅
Worked example How much parsing does modules save?
A header bigheader.h takes 0.5 s to parse. It is #included by 200 translation units. Compare total header-parse time under #include vs a module import bigheader; (parsed once, then reused).
Forecast: guess the two totals and the ratio before reading on.
Step 1 — #include cost. Textual paste re-parses the header in every translation unit: 200 × 0.5 s = 100 s .
Why this step? This is exactly the "re-parsed every time" row of the parent's comparison table — quantified.
Step 2 — module cost. A module's interface is parsed/compiled once into a binary interface, then reused: 1 × 0.5 s = 0.5 s of parsing (the other 199 units just link to the cached interface).
Why this step? "Parsed once" is the core mechanism; we treat the 199 reuses as ~free parse-wise.
Step 3 — speedup ratio. 0.5 100 = 200 × .
Why this step? Turns the abstract win into a number a build engineer can quote — see Build Systems and Compilation Speed .
Verify: #include: 100 s. import: 0.5 s. Ratio = 200 , equal to the number of translation units (as expected — you pay the parse once instead of N times). ✅
Worked example Mark each line ✅ compiles / ✗ error.
// lib.ixx
export module lib;
export int pub () { return 7 ; }
int priv () { return 8 ; }
export { int gp () { return 9 ; } }
// client.cpp
import lib;
int main () {
int a = pub (); // L1
int b = priv (); // L2
int c = gp (); // L3
return a + c; // uses only compiling names
}
Forecast: which of L1, L2, L3 compile, and what does a + c equal?
Step 1 — classify each name. pub exported (single), gp exported (group block), priv not exported.
Why this step? Same single gate as every example — is export present?
Step 2 — mark the lines. L1 pub() ✅ ; L2 priv() ✗ (undeclared, not exported); L3 gp() ✅.
Why this step? Only exported names reach client.cpp.
Step 3 — if L2 is removed, a + c = 7 + 9 = 16.
Why this step? Confirms the arithmetic of the surviving, compiling program.
Verify: a = 7 , c = 9 , a + c = 16 . L2 must be deleted for the file to build. ✅
Recall Answer before expanding
What is the bare module; line (no name) for, and what breaks if you omit it? ::: It opens the global module fragment; omit it and you have nowhere legal to write a #include (E1, E3).
Where is the ONLY legal spot for #include in a module file? ::: In the global module fragment, between module; and export module Name; (E3).
A private member of an exported class — visible to importers? ::: No; the class door (public) blocks it even though the module door (export) is open (E5).
Does import lib; bring in lib's macros? ::: No — a named-module import exports no macros; use a header unit import "h.h"; if you truly need them (E7).
Header unit vs named-module import — which keeps macros? ::: The header unit (import "h.h";) keeps macros; the named-module import (import util;) does not (E7).
Two files both write export module shape; — legal? ::: No; a module has exactly one primary interface unit. Use partitions export module shape:part; (E6).
A header parsed 0.5 s, included in 200 units — #include vs module parse totals? ::: 100 s vs 0.5 s, a 200 × saving (E8).
Mnemonic The whole page in one line
A name is usable outside a module ⇔ it passed the module door (export) and , if it's a class member, the class door (public) — and #include only ever lives in the quarantine band opened by the bare module; line.