C++ has historically not had a real module system. The textual #include model — paste headers everywhere, parse them again per TU — is the root cause of long build times and most ODR-related bugs. C++20 modules finally fix this; this page covers both the modern story and the pragmatic mixed reality.
- 1. Why Modules Matter
- 2. Module Basics
- 3. Module Partitions
- 4. Header Units — Migration Bridge
- 5. Build-System Architecture: Targets, Layers, Boundaries
- 6. CMake and Modules
- 7. Toolchain Reality (2024+)
- 8. Compile-Time Strategy Without Modules
- 9. Quick Reference
The pre-modules world:
- Every TU re-parses every transitively-included header.
- Macros leak across includes, polluting consumers.
- Order of
#includecan change semantics. - Templates in headers are recompiled in every consumer.
- Build times scale superlinearly with dependency depth.
What modules give you:
- Each module is parsed once. Imports replay a precompiled binary representation.
- No macro leakage. A module's macros do not escape unless explicitly exported.
- Order of imports doesn't matter. Imports are not transitive by default.
- Hidden symbols stay hidden. The compiler enforces
export/non-export. - Faster builds — usually 2–5× on header-heavy code.
// math.cppm — module interface unit
export module math;
export int add(int a, int b) { return a + b; }
export int sub(int a, int b) { return a - b; }
// not exported — visible only inside the module
int internal_helper() { return 42; }// main.cpp
#include <iostream>
import math;
int main() {
std::cout << add(1, 2) << '\n'; // 3
std::cout << sub(5, 3) << '\n'; // 2
// internal_helper(); // error: not exported
}Compile with (Clang):
clang++ -std=c++20 --precompile math.cppm -o math.pcm
clang++ -std=c++20 -fprebuilt-module-path=. main.cpp math.pcm -o appGCC and MSVC have analogous flags. CMake 3.28+ has direct support; see §6.
Two TU kinds:
- Module interface unit (
export module foo;) — declares the module and its exports. Exactly one per module. - Module implementation unit (
module foo;) — implements the interface. May be many.
Big modules can be split into partitions:
// math-arith.cppm
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.cppm
export module math:trig;
export double sin(double x); // implementation elsewhere
// math.cppm — primary interface
export module math;
export import :arith; // re-export the partition
export import :trig;
// consumer
// import math; → sees add, sub, sin as one modulePartitions look to the outside like one module (import math;). Internally they're separately compiled units. Use partitions to break up large modules without exposing the split to consumers.
You can import a legacy header as a module, getting most of the perf win without rewriting:
import <vector>; // standard header as module — fast
import <iostream>;
import "legacy.h"; // your own header
int main() {
std::vector<int> v{1, 2, 3};
for (int x : v) std::cout << x << ' ';
}The compiler precompiles the header to a binary module interface. Macros from header units do leak (they're still header semantics) — but parsing happens once. This is the practical migration path: switch consumers to import, leave the headers themselves alone, then move headers one at a time to real modules.
Modules don't fix bad architecture. The principles are the same regardless:
Layered architecture. Each layer depends only on layers below it. Drawn as a DAG; no cycles.
[ ui ] ──▶ [ application ] ──▶ [ domain ] ──▶ [ infrastructure ]
One build target per logical component. Each gets its own include path, its own dependencies. CMake add_library(domain STATIC ...). Tests at component level.
Public vs private dependencies. In CMake:
target_link_libraries(mylib PUBLIC foo)—foois part of mylib's interface; consumers see it.... PRIVATE foo— used internally only.... INTERFACE foo— header-only / pure transitive.
Misuse causes leaky abstractions. A core domain library should not have PUBLIC dependencies on infrastructure libraries.
Convention for big projects:
project/
├── components/
│ ├── domain/
│ │ ├── include/proj/domain/ # public
│ │ ├── src/ # private
│ │ ├── test/
│ │ └── CMakeLists.txt
│ ├── infra/...
│ └── ui/...
├── apps/
│ └── server/CMakeLists.txt
└── CMakeLists.txt # ties it all together
CMake 3.28 added first-class C++20 modules support:
cmake_minimum_required(VERSION 3.28)
project(modules_demo CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
add_library(math)
target_sources(math
PUBLIC FILE_SET CXX_MODULES FILES
math.cppm
math-arith.cppm
math-trig.cppm
)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE math)FILE_SET CXX_MODULES is the new construct. CMake handles dependency scanning, ordering, and BMI (binary module interface) caching across the build.
You need a recent toolchain — clang-16+, gcc-14+, MSVC 19.40+, and Ninja 1.11+ (for dyndep).
Status as of mid-2024:
- Clang/libc++: best-supported.
import std;works. Module-awareclangdfor IDE. - GCC/libstdc++: full module support landed in GCC 14. Some standard headers as modules still incomplete.
- MSVC/STL: well-supported, including
import std;. Visual Studio integrates cleanly. - CMake: 3.28+ for direct support; 3.30 fixed many corners.
- Ninja: required for module dependency scanning (Make-based builds don't work cleanly).
Mixed projects:
- Modules and headers coexist. A module can
#includelegacy headers; a legacy TU canimportmodules. - Don't define macros in module-purview that you want consumers to see — use a
module : private;section or convert toconstexprconstants. - Be careful with
export import std;if other consumers also have their own STL bindings.
If you can't use modules yet, the long-standing tools:
- Forward declarations wherever possible.
- PIMPL for big classes — header sees only
unique_ptr<Impl>. - Precompiled headers (
#pragma once+target_precompile_headers) — biggest non-module speedup. - Unity builds (CMake
CMAKE_UNITY_BUILD) — concatenate TUs to amortize header parsing. Trades memory for time. include-what-you-use— IWYU clean up reduces transitive includes.ccache/sccache— cache compiler output across builds.
Combined, these cut typical C++ build times in half before you change a line of architecture.
| Want | Do |
|---|---|
| Fastest builds, fresh codebase | Modules from day one, import std;, CMake 3.28 |
| Faster builds, mature codebase | PCH first, then header units, then modules incrementally |
| Hide internal symbols | Don't export; module privacy is enforced |
| Split a big module | Partitions (module foo:bar) |
| Keep a stable consumer interface | One module per public component, internal partitions for splits |
| Detect dependency cycles early | Per-component CMake targets, no PUBLIC cycles |
- API and ABI Design
- Design a Reusable Library
- Modules
- PIMPL
- CMake C++ Modules support
- Working Draft / P1103, A Module System for C++
- Large-Scale C++ Software Design, John Lakos.