C++ Modules (C++20)
- Description: A note on C++20 modules —
import,export, module partitions, header units, the global module fragment, and migration from#include - My Notion Note ID: K2A-B1-27
- Created: 2022-03-15
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Why Modules?
- 2. Importing a Module
- 3. Defining a Module
- 4. Module Partitions
- 5. Header Units
- 6. Global Module Fragment
- 7. Migration from
#include - 8. Compiler and Build-System Support
1. Why Modules?
#include literally pastes header text into every translation unit (TU). For a large project, the same headers are reparsed thousands of times, and any non-trivial macro can leak into unrelated code.
Modules solve both:
- One-time compilation. A module is parsed once, then imported as a precompiled artifact (a BMI — Binary Module Interface).
- Hard isolation. Macros, internal symbols, and unrelated declarations don't leak across the module boundary.
- Faster builds. No re-parsing the same headers in every TU.
- Cleaner ABI control. Only
exported symbols are visible to importers.
Modules are not a runtime feature — they change the build model, not the generated code.
2. Importing a Module
import std; // C++23: the entire standard library as one module
import std.compat; // C++23: same as std plus C library names in the global namespace
import math; // your own module
import :helpers; // a module partition (only inside another module)
import looks like a statement, but it must appear at the top of the file (after any global module fragment). It does not introduce names into the current scope — only the names the imported module exports.
3. Defining a Module
A module's primary interface unit declares the module and what it exports.
// math.cppm (the .cppm extension is convention; not required)
export module math; // primary interface
export int add(int a, int b) { // exported, visible to importers
return a + b;
}
int internal(int x) { // NOT exported; internal to this TU
return x * 2;
}
export { // export multiple declarations at once
int sub(int a, int b);
int mul(int a, int b);
}
Implementation files (no export keyword on the module declaration) provide bodies for declarations:
// math_impl.cpp
module math; // implementation unit (no `export`)
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
4. Module Partitions
Large modules can be split into partitions — submodules that share the parent module's name.
// math-trig.cppm
export module math:trig; // partition of math
export double sin(double x);
export double cos(double x);
// math.cppm
export module math;
export import :trig; // re-export the partition
Importers see one logical module:
import math;
math::sin(0.5);
Partitions can also be private (not re-exported):
// math.cppm
export module math;
import :impl_helpers; // not re-exported; for use only inside math
5. Header Units
Header units let you import an existing header as if it were a module — useful during migration.
import <vector>; // standard header as a module
import "myheader.h"; // your header as a module
Macros from the header still work (header units preserve them, unlike named modules), but you get faster compilation. Header units are a stepping stone, not the final destination.
6. Global Module Fragment
Sometimes a module needs to use a #include-only header (one that hasn't been modularized). The global module fragment at the top lets you do this:
module; // begin global module fragment
#include <vector> // legacy header
#include "legacy_macro.h" // bring in macros, types, etc.
export module myapp; // begin the module proper
import std; // can also use proper imports
export void foo(std::vector<int>);
The global module fragment is a holdover for legacy interop. New code should prefer import and import <header>;.
7. Migration from #include
A practical migration path for an existing codebase:
- Convert the heaviest, most-included headers first (the ones in many TUs' include graph). Convert them to header units (
import <header>;) for an easy win. - Modularize internal libraries as named modules. Drop
#include "internal.h"in favor ofimport internal;. - Move public APIs to modules last, since their interface and ABI are more visible.
- Don't mix lifestyles needlessly — once a header is a module, its consumers should use
import.
Module-aware compilers can usually still consume #include for unported headers, so you don't need a flag day.
8. Compiler and Build-System Support
As of late 2025:
- MSVC — strongest support. Named modules work with
/std:c++20.import std;requires/std:c++latest(C++23 standard library named modules). - Clang — named modules solid since Clang 16;
import std;since Clang 18 (with libc++ configured to ship the std module). - GCC — improving, but
import std;and some advanced features lag the others. - CMake — named-module support added in 3.28 (controlled by
CMAKE_CXX_SCAN_FOR_MODULES);import std;integration added in CMake 3.30. - Build systems other than CMake — Bazel, Meson, etc. each have their own module integrations; check vendor docs.
Reality check: modules deliver real build-time wins, but the toolchain is still maturing. Most existing codebases are still on #include. Expect to mix both for a while.