C++20 Modules: Replace #include with import
Learn to write, import, and partition C++20 modules — replacing brittle header files with encapsulated, fast-compiling translation units that control what they export.
By the end of this page you will write a primary module interface unit, export declarations from it, import it in another translation unit, split large modules into partitions, and understand the key differences from the header-based model.
What and Why
The #include model has a fundamental problem: a header is textually inserted into every translation unit that includes it. This means:
- The preprocessor re-parses the same code thousands of times.
- Macros in one header bleed into others.
- Everything the header defines becomes visible — even "implementation details" you didn't intend to export.
- Circular dependencies require forward declarations and include-order discipline.
C++20 modules solve all four problems. A module is compiled once; its interface is saved in a binary artifact (a compiled module interface, CMI) and reused by every importer. Only explicitly exported declarations are visible outside the module. Macros do not cross module boundaries. And the module can control its internal structure freely.
// ── math.cpp ─────────────────────────────────
export module math; // primary module interface unit
export int square(int x) { return x * x; }
export int cube(int x) { return x * x * x; }
int helper(int x) { return x + 1; } // NOT exported — internal
// ── main.cpp ─────────────────────────────────
import math;
int main() {
return square(4); // 16
// helper(1); // error: helper is not visible
}Step by Step
The primary module interface unit
A translation unit that begins with export module name; is the primary module interface unit (PMIU). Every named module must have exactly one.
// geometry.cpp
export module geometry; // declares this TU as the PMIU for module 'geometry'
import <cmath>; // import standard headers as header units
export struct Point {
double x, y;
};
export double distance(Point a, Point b) {
double dx = a.x - b.x, dy = a.y - b.y;
return std::sqrt(dx * dx + dy * dy);
}
// Internal — not exported
static double sq(double v) { return v * v; }Module implementation units
You can split implementation from the interface in a module implementation unit (no export on the module declaration):
// geometry_impl.cpp
module geometry; // implementation unit — no 'export'
// Definitions here have access to all declarations in geometry's interface
// but add no new exportsImporting a module
// main.cpp
import geometry;
import <iostream>; // standard headers as header units
int main() {
Point a{0, 0}, b{3, 4};
std::cout << distance(a, b) << '\n'; // 5
}import makes all exported declarations from geometry visible. Non-exported names (sq) are not accessible.
Export blocks
Group multiple declarations under a single export { }:
export module shapes;
export {
class Circle { /* ... */ };
class Rectangle { /* ... */ };
double area(const Circle&);
double area(const Rectangle&);
}You can also re-export another module's interface with export import:
export module geometry_full;
export import geometry; // importers of geometry_full also see geometry's exports
export import shapes;Global module fragment — including legacy headers
Macros do not cross module boundaries. If you need to #include a header that uses macros internally, put it in the global module fragment — the section before the module declaration:
// wrapper.cpp
module; // starts global module fragment
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h> // macros processed here, before module begins
export module wrapper; // end of global module fragment
import <ctime>;
export double safe_random() {
// drand48() from <stdlib.h> is usable here
return drand48();
}Module partitions — splitting a large module
A large module can be divided into partitions. Partitions are module units that belong to a named module but are only visible within that module:
// math-arith.cpp — partition ':arith'
export module math:arith;
export int add(int a, int b) { return a + b; }
export int sub(int a, int b) { return a - b; }
// math-trig.cpp — partition ':trig'
export module math:trig;
import <cmath>;
export double sin_deg(double d) { return std::sin(d * 3.14159 / 180.0); }
// math.cpp — primary module interface unit
export module math;
export import :arith; // re-export arith's declarations to module importers
export import :trig; // re-export trig's declarationsNow import math; gives access to all of arith's and trig's exports.
Common Patterns
Hiding implementation details
Without modules, private only protects class members. Internal helper functions in a header are visible to every includer. With modules, only exported names are visible:
export module crypto;
// These are NOT exported — users cannot call them directly
void xor_block(uint8_t* a, const uint8_t* b, std::size_t n);
uint32_t rotate_left(uint32_t v, int n);
export void encrypt(std::span<std::byte> data, std::span<const std::byte> key);Standard library modules (C++23)
C++23 provides two pre-built modules that replace the entire standard library headers:
import std; // the whole standard library
import std.compat; // std + global-scope legacy names (printf, etc.)On supported toolchains these are dramatically faster to import than dozens of #include directives.
What Can Go Wrong
Build system support is still maturing
Modules require the build system to understand dependency order between module units before compilation starts — a harder problem than tracking #include dependencies. CMake 3.28+ supports modules via target_sources(... FILE_SET CXX_MODULES ...); Clang, GCC 14+, and MSVC all support the language feature but toolchain integration varies.
Check your compiler's module support status before committing to modules in a large codebase.
#include inside a module unit (outside the global fragment)
Including a header inside a module unit makes all its declarations part of the module's reachable but not exported set — it can cause ODR problems if the same header is also included in non-module TUs. Prefer import <header> (header units) inside module units, or put #include in the global module fragment.
Macros cannot be exported
This is intentional — macros are not part of the module's semantic interface. If your API relies on macros (e.g., ASSERT, LOG_LEVEL), those must stay in headers.
One primary module interface unit per module
// a.cpp
export module foo; // OK: PMIU
// b.cpp
export module foo; // ERROR: second PMIU for 'foo'Only the primary interface unit may omit a partition suffix.
Build System Setup
CMake 3.28+
CMake 3.28 was the first release to ship non-experimental support for C++20 named modules. The key change is registering module source files in a FILE_SET of type CXX_MODULES instead of the normal source list. CMake then runs a dependency scanner (P1689 protocol) on those files before compilation begins, so it knows the correct build order without any hand-written dependency hints.
Here is a complete two-file example — a library exposing module math and an executable that imports it:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(math_example CXX)
# Disable compiler extensions — required for Clang 16 module support
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD 20)
# --- Library target ---
add_library(math)
# Module interface files go in FILE_SET CXX_MODULES, not in plain sources
target_sources(math
PUBLIC
FILE_SET CXX_MODULES FILES
src/math.cpp # export module math;
src/math-arith.cpp # export module math:arith;
)
# --- Executable target ---
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE math)The src/math.cpp module interface unit:
// src/math.cpp
export module math;
export import :arith; // re-export the arith partitionThe src/math-arith.cpp partition:
// src/math-arith.cpp
export module math:arith;
export int square(int x) { return x * x; }
export int cube(int x) { return x * x * x; }The src/main.cpp consumer (a regular source file, not in FILE_SET CXX_MODULES):
// src/main.cpp
import math;
#include <cstdio>
int main() {
std::printf("%d\n", square(5)); // 25
}Generator requirement: CMake 3.28 module support requires the Ninja generator or a Visual Studio 2022 generator. The Makefile generator does not support C++20 modules.
Supported compilers: MSVC 14.34+ (VS 17.4+), Clang 16+, GCC 14+.
If you need to use C++20 features without modules and want to suppress the new dependency-scanning overhead, opt out explicitly:
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)Import Forms: Named Modules, Header Units, and #include
C++20 provides three ways to bring declarations into a translation unit. They differ in speed, macro visibility, and how much they isolate implementation details.
| Form | Syntax | Macros leak out? | Macros affect processing? | Speed |
|---|---|---|---|---|
| Named module | import math; | No | No | Fastest (binary CMI reused) |
| Header unit | import <vector>; | Yes | No* | Fast (like PCH) |
| Old include | #include <vector> | Yes | Yes | Slowest (re-parsed every TU) |
* Macros defined before the import <header> line do not affect how the header is processed — unlike #include. However, the header's own macros are still exported to the importer.
Named modules (import math;) are the full solution: compiled once to a binary compiled module interface (CMI), expose only exported names, and pass no macros outward.
Header units (import <vector>;) are a transitional bridge. The compiler synthesises a module-like artifact from the header but still exports its macros. They are significantly faster than #include and do not require any source changes to the header itself. Not every header is importable — the standard only guarantees that all C++ standard library headers (<vector>, <string>, etc.) are importable; C headers like <stdlib.h> are not.
#include in the global module fragment is still the right tool for headers that use macros as configuration knobs (e.g. #define _POSIX_C_SOURCE), because header units are immune to macros already defined at the point of import:
module;
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h> // must be #include here — header unit would ignore the define
export module posix_wrap;
// ...The rough progression from slowest/least-isolated to fastest/most-isolated:
#include → precompiled header → import <header> → import module;Migrating from Headers
Migrating a library to modules works best incrementally — you can keep the existing headers compiling while progressively converting files.
Step 1: Wrap in a module using the global fragment
Take your existing header pair (widget.hpp / widget.cpp) and create a thin module interface unit that #includes the header in the global fragment. This makes the library importable without touching the header at all:
// widget_module.cpp
module;
#include "widget.hpp" // existing header goes in global fragment
export module widget;
// Re-export the names you want to expose
export using ::Widget;
export using ::make_widget;Users can now write import widget; while the internal headers remain unchanged.
Step 2: Remove #include internals one by one
Once the module builds, start replacing #include lines inside the interface with import or with direct declarations. Convert standard library headers first since they are guaranteed importable:
module;
// #include <vector> — removed; use import instead
#include "legacy_c_api.h" // keep: C header, not importable as header unit
export module widget;
import <vector>; // now a header unit — faster
import <string>;
export class Widget {
std::vector<std::string> items_;
public:
void add(std::string s);
};Step 3: Switch to import std; (C++23)
Once the codebase is on a C++23 toolchain with standard library module support, collapse all standard library includes into a single line:
module;
// all the old #include <...> lines gone
export module widget;
import std; // entire standard library — fastest option
export class Widget { /* ... */ };Tips:
- Keep the old
.hppcompiling for downstream consumers that haven't migrated yet — use#ifdefguards or a compatibility shim. - Migrate leaf libraries (no internal
#includechains) first; work toward the root. - Module implementation units (
module widget;, noexport) let you keep.cppfiles almost unchanged during transition.
Private Module Fragment
When a module is small enough to live in a single file but you still want to separate the exported interface from the implementation, use module : private;. Everything after that line is part of the private module fragment — it provides definitions used by the interface but is not reachable by importers, and changes to it do not force recompilation of importing translation units.
// math.cpp — single-file module with private implementation
module;
#include <cmath>
export module math;
// Public interface — visible to all importers
export double hypotenuse(double a, double b);
export double deg_to_rad(double degrees);
module : private; // ← starts private module fragment
// nothing below here is reachable from importers
double hypotenuse(double a, double b) {
return std::sqrt(a * a + b * b);
}
double deg_to_rad(double degrees) {
return degrees * (3.14159265358979 / 180.0);
}Key constraints (from the standard):
- A module unit that contains
module : private;must be the only module unit of its module — you cannot also have separate partition files or implementation units. - The private fragment may contain definitions of previously declared entities, static helpers, and anything else that belongs to the implementation — as long as none of it is exported.
- This is the single-file equivalent of the interface + implementation unit split. Use the two-file split instead when you want multiple implementors or when the implementation is large.
Quick Reference
| Syntax | Purpose |
|---|---|
export module name; | Declare primary module interface unit |
module name; | Declare module implementation unit |
export module name:part; | Declare exported partition |
module name:part; | Declare non-exported partition |
export T f(); | Export a single declaration |
export { ... } | Export a block of declarations |
import name; | Import a module |
import <header>; | Import standard library header as header unit |
export import name; | Re-export another module |
module; | Begin global module fragment (for #include) |
module : private; | Begin private module fragment |
import std; | Import entire standard library (C++23) |
Compiler support: GCC 14+, Clang 16+, MSVC 19.34+. Build system support: CMake 3.28+, build2, MSBuild.
What's Next
- Compilation Model — how translation units, the preprocessor, and the linker work together.
- ODR and Linkage — the one-definition rule that modules help enforce more naturally.
- C++20 Features — the full set of C++20 additions alongside modules.