C++20 modules require a toolchain and build system that understand module dependency order (CMake 3.28+, recent GCC 13+, Clang 16+, or MSVC 19.34+). Header-based builds remain the portable default.
For four decades, C++ relied on a compilation model inherited directly from C: the preprocessor based header system. When you write #include <iostream>, the preprocessor textually includes declarations into a translation unit. C++20 Modules offer a compiled interface model that can improve dependency isolation and build times, but they coexist with headers in real codebases rather than making headers disappear overnight.
Let's look at translation unit isolation, compile bloat, and how to build modules.
The Problem with Headers: Compilation Bloat
When using standard headers, code compilation suffers from two main problems:
* Compilation Slowness: Every single file (called a translation unit) compiles in isolation. If multiple files include the same headers, the compiler must parse, check, and generate code for those headers repeatedly. This leads to hours of compile time for massive systems.
* Macro Pollution and Name Clashes: Because headers are simple copy paste operations, macro definitions and internal namespace details leak into your files, causing name clashes and silent compiler overrides.
How Modules Solve This: Compiled Interfaces
A module is declared using the export module statement. Instead of raw text copy pasting, the compiler parses a module file (like Math.cppm) exactly once. It creates an Abstract Syntax Tree (AST) representation of the code, and serializes this into a binary module interface file (.pcm on Clang, .gcm on GCC, or .ifc on MSVC).
When another file writes import Math;, the compiler consumes the module interface produced by its toolchain instead of textually re-parsing a header. The exact artifact format and performance characteristics are implementation details, but imported macros do not leak through the named-module interface.
Let's see how to write a simple module interface and import it:
File Extensions and Build System Compilation
Unlike standard headers, compiling C++ modules is toolchain dependent. While the C++ ISO standard does not force a specific file extension, compiler vendors have established conventions:
- GCC and Clang commonly look for .cppm (C++ Module) or standard .cpp extensions.
- Microsoft Visual C++ (MSVC) expects .ixx for module interfaces.
Because the compiler must generate a module interface before importers can build, a build tool must understand dependency order. Current CMake, build2, and compiler-specific build flows can do this; the exact commands remain toolchain-dependent.
The Global Module Fragment: Resolving Legacy Headers
What if you need legacy preprocessor headers in a module file? Headers and named modules can coexist, but a textual include after the module declaration may attach declarations to that module and create surprising build or linkage behavior. The Global Module Fragment is the explicit place for includes that must remain outside the named module:
C++23 Standard Library Modules
C++23 specifies named standard-library modules such as std, but implementation support is still uneven. Treat import std; as a toolchain feature: verify support in your compiler, standard library, and build configuration before adopting it.
Interface vs Implementation
Modules allow you to separate the exported interface (what users see) from the actual code implementation (how it works) in a single file or across separate files, without using headers. Only symbols marked with the export keyword are visible to importers. All other classes, structures, and helper functions remain completely hidden, enforcing strong encapsulation at the module boundary.
* The Library Index Metaphor: Preprocessor header inclusion is like photocopying an entire dictionary page into every letter you write just to use one word. C++20 modules are like placing a small reference index sticker. You look up the word directly in the precompiled dictionary without copying any pages.