Modules
Before C++20, code organization rested on #include — a mechanism inherited all the way from C. Every #include literally copies the text of a header into a translation unit. This worked for decades, but by the late 2010s it had become clear: the model does not scale. C++20 modules are the answer to this problem.
Why modules appeared
To understand modules, you first need to understand what exactly is broken about #include.
Problems of the #include model
Imagine your project has 500 files, and each one includes <vector>, <string>, <map>. The compiler re-parses all of these headers from scratch — millions of lines of code — when compiling every single translation unit.
// main.cpp — the compiler parses all these headers from scratch
#include <vector> // ~15,000 lines after expansion
#include <string>
#include <map>
#include "my_lib.h" // and your header too
But slow compilation is not the only trouble:
ODR violations (One Definition Rule). The C++ rule requires every entity to be defined exactly once. With headers this is easy to break: two .cpp files include one header holding a function definition without inline — the linker reports an error (or, worse, silently picks one of the definitions).
Macro leakage. #define knows no scope. If one header defines #define max(a, b) ..., it breaks any code below that uses max as a function or method name.
// somewhere in windows.h
#define min(a, b) ((a) < (b) ? (a) : (b))
#include <windows.h>
#include <algorithm>
// std::min stops working — the macro "hijacks" the call
Include order. If header A defines a macro that changes the behavior of header B, then #include "a.h" before #include "b.h" gives a different result than the reverse. This is fragile.
No real encapsulation. Everything declared in a header is visible to everyone who includes it — including helper details that "should not be part of the API".
The #include model vs modules
Modules solve all of this at the standard level: the interface is compiled once into a Binary Module Interface (BMI), macros do not cross module boundaries, and only what is explicitly marked gets exported.
Basic module structure
A minimal module consists of two parts: the module declaration and its use.
The module interface
// math.cppm — the module interface (extension .cppm or .ixx)
export module math; // declare the named module "math"
export double square(double x) { return x * x; }
export double cube(double x) { return x * x * x; }
// This function is NOT exported — it is an internal implementation detail
double helper(double x) { return x + 1.0; }
Keywords:
export module math;— the module declaration (must come first)exportbefore a function or class — makes them part of the module's public API- Without
export— the entity is visible only inside the module
Using the module
// main.cpp
import math; // import the module — the BMI is already compiled
int main() {
double a = square(3.0); // 9.0 — available because of export
double b = cube(2.0); // 8.0 — available because of export
// helper(1.0); // compile error — not exported
}
Manual compilation (Clang):
# Step 1: compile the interface into a BMI
clang++ -std=c++20 --precompile math.cppm -o math.pcm
# Step 2: compile the object file from the interface
clang++ -std=c++20 -fmodule-file=math=math.pcm -c math.cppm -o math.o
# Step 3: compile and link the consumer
clang++ -std=c++20 -fmodule-file=math=math.pcm main.cpp math.o -o main
What can be exported
export works for most top-level declarations:
export module mylib;
// Functions
export void foo();
export int bar(int x, int y);
// Classes and structs
export class Widget {
public:
void render();
private:
int id_; // private stays private — a module does not change access rules
};
// Templates
export template<typename T>
T identity(T x) { return x; }
// Type aliases
export using Id = unsigned int;
// export block — several declarations at once
export {
void start();
void stop();
struct Config { int timeout; int retries; };
}
// Enumerations
export enum class Color { Red, Green, Blue };
You cannot export from an anonymous namespace, nor static entities (they have internal linkage by definition).
Separating interface and implementation
The interface and implementation can be split across different files:
// math.cppm — interface (module interface unit)
export module math;
export double square(double x);
export double cube(double x);
// math_impl.cpp — implementation (module implementation unit)
module math; // without export — this is the implementation unit, not the interface
double square(double x) { return x * x; }
double cube(double x) { return x * x * x; }
Consumers only need to know the interface — they do not see the implementation unit and do not compile it directly.
Module Partitions
A large module can be split into partitions — logical parts that together form a single module:
// math-basic.cppm — the :basic partition
export module math:basic;
export int add(int a, int b) { return a + b; }
export int sub(int a, int b) { return a - b; }
// math-advanced.cppm — the :advanced partition
export module math:advanced;
export double power(double base, int exp);
export double sqrt_approx(double x);
// math.cppm — the primary interface that assembles the partitions
export module math;
export import :basic; // re-export the partition to consumers
export import :advanced;
The consumer simply writes import math; and gets everything. Partitions are an organizational detail inside the module.
A module's structure with partitions
The global module fragment — compatibility with #include
Modules cannot use #include inside their body directly — that would break isolation. For including legacy headers there is a special mechanism: the global module fragment.
// Global module fragment — before the module declaration
module; // opens the global module fragment
#include <cstdlib> // legacy headers go here
#include <cassert>
#include "legacy_lib.h"
// Now declare the module
export module mylib;
// Here you can use everything from the included headers,
// but they do NOT become part of the module's export
export void do_something();
Important: entities from #include inside the global module fragment are not exported to the module's consumers. They are visible only inside the implementation unit.
export import — re-export
A module can re-export other modules to its consumers:
export module geometry;
export import math; // consumers of geometry automatically get math
export import shapes;
export struct Point { double x, y; };
export struct Rect { Point origin; double w, h; };
This is convenient for building "facade" modules that combine several submodules into a single interface.
Header Units — a bridge to legacy code
If rewriting all dependencies as modules is not yet possible, import can also be used with ordinary headers:
import <vector>; // a header unit from the standard library
import <iostream>;
import "my_header.h"; // a header unit from your own code (support varies)
A header unit is a compiled header in BMI format. It removes the repeated parsing, but macros from a header unit are still available to the consumer — this is what distinguishes them from true modules.
Binary Module Interface (BMI)
BMI is a binary file that the compiler creates from the module interface. It is exactly what consumers read instead of the source .cppm.
| Compiler | BMI extension | Flags |
|---|---|---|
| GCC 14+ | .gcm | automatic |
| Clang 16+ | .pcm | --precompile, -fmodule-file=name=file.pcm |
| MSVC 2019+ | .ifc | /interface, /reference name=file.ifc |
math.cppm ──compiler────▶ math.pcm (BMI)
│
┌──────────────┼──────────────┐
▼ ▼ ▼
unit_a.cpp unit_b.cpp unit_c.cpp
(reads BMI, (reads BMI, (reads BMI,
not source) not source) not source)
The speedup happens because a BMI is an already-parsed representation of the module's semantics. The compiler does not parse tokens again; it reads a ready-made structure.
Important: BMI files are not portable between compilers, and often not even between versions of the same compiler. Do not store BMIs in the repository — the build system generates them.
Support in compilers and build systems
C++20 modules require explicit support not only in the compiler but also in the build system — because the build became dependency-oriented: the BMI must be created before compiling the consumers.
| Tool | Status |
|---|---|
| GCC 14+ | Full support for named modules |
| Clang 16+ | Full support |
| MSVC VS 2022 17.5+ | Full support |
| CMake 3.28+ | Native support (target_sources(... FILE_SET CXX_MODULES)) |
| Build2 | Module support from early versions |
| Meson | Support added in 1.3.0 |
| Make / Autotools | Practically none — you have to order compilation manually |
CMake 3.28 example:
cmake_minimum_required(VERSION 3.28)
project(myapp CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
add_executable(myapp main.cpp)
target_sources(myapp
PUBLIC FILE_SET CXX_MODULES FILES math.cppm
)
Practical limitations and pitfalls
BMIs are not portable. You cannot hand an .ifc from MSVC to GCC. Every toolchain rebuilds the BMI itself.
You cannot export macros. This is deliberate — one of the main goals of modules. If your API depends on macros, a module will not help without an additional layer.
Be careful with include inside a module without a global fragment. Some compilers will allow #include inside the module body, but the behavior is non-standard and undesirable.
Cyclic imports are forbidden. If A imports B, B cannot import A. This is a deliberate restriction — the dependency graph must be acyclic.
Mixing with headers takes care. If one .cpp uses #include "foo.h" and another uses import foo_module (which includes the same code), you can get an ODR violation.
Legacy code does not migrate automatically. Moving a large codebase to modules is a gradual process. Start with leaf dependencies (those with no outgoing dependencies in your code) and work your way up the graph.
Interview relevance
In an interview, C++20 modules most often come up in the context of a conversation about the C++20 standard as a whole, or about the problems of large projects. The interview-importance here is low — most companies have not yet moved to modules in production — but understanding why they appeared signals a candidate's level.
What the interviewer is checking:
- Whether the candidate understands the problems of the
#includemodel (slow compilation, ODR, macro pollution) — rather than just knowing the syntax - Whether they know the difference between a module interface unit and a module implementation unit
- Whether they understand what a BMI is and why it speeds up the build
- Whether they know the real limitations — compiler support, build systems, BMI non-portability
Popular directions for questions:
- "What problems do modules solve — and which do they not?" (ODR, macros, speed — yes; replacing all
#includeat once — no) - "What is a BMI and how does it differ from precompiled headers?"
- "What is a module partition and why is it needed?"
- "What is the global module fragment?"
- "Why can't you export macros through a module?"
Common mistake. The candidate says: "Modules are just a better #include." That is wrong. The key difference is semantic isolation: import does not carry macros, only explicitly marked things are exported, and there is no order dependency. It is a fundamentally different model, not an improvement on textual substitution.
Full example: the math module
// === math.cppm ===
export module math;
export int gcd(int a, int b) {
while (b) { a %= b; std::swap(a, b); }
return a;
}
export int lcm(int a, int b) {
return a / gcd(a, b) * b;
}
// Helper — not exported
namespace detail {
bool is_coprime(int a, int b) { return gcd(a, b) == 1; }
}
// === main.cpp ===
import math;
// #include <iostream> — iostream is not yet a module in most implementations
// use a global module fragment in a separate file, or just #include
#include <iostream>
int main() {
std::cout << gcd(12, 8) << "\n"; // 4
std::cout << lcm(4, 6) << "\n"; // 12
}
Compilation with GCC 14:
g++ -std=c++20 -fmodules-ts -c math.cppm
g++ -std=c++20 -fmodules-ts main.cpp math.o -o main
Compilation with Clang 16:
clang++ -std=c++20 --precompile math.cppm -o math.pcm
clang++ -std=c++20 -fmodule-file=math=math.pcm -c math.cppm -o math.o
clang++ -std=c++20 -fmodule-file=math=math.pcm main.cpp math.o -o main
In brief
C++20 modules change the basic model of code organization: instead of textual substitution via #include, you get semantically isolated units with explicit export. The compiler creates the BMI once and hands it to every consumer; macros do not leak; import order does not matter. The main barrier today is not the compilers (GCC 14, Clang 16, MSVC 2022 are already ready) but the build systems and legacy codebases, which migrate gradually. Understanding modules as a "better #include" is a mistake: it is a different model of encapsulation, not syntactic sugar.