C++ Modules Introduction
Learn C++20 modules: what they replace, how to declare and import them, and how they speed up compilation and clean up the preprocessor.
What you'll learn
- ✓Why modules exist
- ✓Module interface vs implementation
- ✓export and import
- ✓Build-system support
- ✓Migration tips
Prerequisites
- •Basic familiarity with C++
What and Why
For decades, C++ code-sharing has relied on the preprocessor: headers, include guards, and macro hygiene. Headers are textually pasted into every translation unit that includes them. The result is slow builds, fragile macros, and ODR-violation landmines.
C++20 modules replace this with a real component system. A module is compiled once, exports a curated set of names, and is consumed via import without textual inclusion.
Mental Model
Think of modules as compiled libraries with a clean public interface. The compiler produces a Binary Module Interface (BMI) for each module; importers read the BMI instead of reparsing source.
Headers: Modules:
foo.h --included into--> foo.cppm --compiled to--> foo.bmi
a.cpp (parsed) a.cpp imports foo (reads BMI)
b.cpp (parsed) b.cpp imports foo (reads BMI)
c.cpp (parsed) c.cpp imports foo (reads BMI)
(parsed N times) (parsed once) Hands-on Example
A module interface unit (math.cppm):
export module math;
export int add(int a, int b) { return a + b; }
int internal_helper() { return 42; } // not exported
export namespace geom {
double area(double r);
}
A consumer (main.cpp):
import math;
#include <iostream>
int main() {
std::cout << add(2, 3) << "\n";
// internal_helper(); // ERROR: not exported
}
Compile with Clang:
clang++ -std=c++20 --precompile math.cppm -o math.pcm
clang++ -std=c++20 -fmodule-file=math=math.pcm main.cpp math.cppm -o app
GCC and MSVC have analogous flags. CMake 3.28+ has native support via add_library(... FILE_SET CXX_MODULES ...).
Common Pitfalls
Mixing headers and modules badly. You can #include <vector> inside a module, but every translation unit that does will reparse it. Prefer import std; (C++23) or wrap legacy headers with import <vector>; (header units) where supported.
Macros do not cross modules. That is a feature: imports never leak macros into the importer. But it means library-style macros must become regular code or be redeclared per file.
Build system support is uneven. Module support shipped late in CMake, Bazel, and Meson. Verify your toolchain (Clang 16+, GCC 14+, MSVC 19.34+) and build system before adopting widely.
Order matters. Module BMIs must be built before consumers. Most build systems handle dependencies automatically, but ad-hoc Makefiles will not.
Circular imports. Modules cannot import each other circularly. This forces healthier dependency graphs but can require refactoring during migration.
Practical Tips
- Start with new code in a leaf library. Don’t try to migrate an entire codebase at once.
- Use the global module fragment (
module;block) to include legacy headers safely:
module;
#include <legacy.h>
export module wrapper;
export void wrapped() { /* uses legacy */ }
- Keep one logical concept per module. Modules don’t need to mirror file organization, but small focused modules give you the biggest build-speed wins.
- Use
export module foo:utils;for module partitions to split a large module across files. - Measure compile times before and after; the gains are real but vary by codebase.
Wrap-up
Modules are the most significant change to C++ build mechanics since templates. They fix problems we’ve lived with for decades: header parse time, macro pollution, ODR violations from accidental include order. Tooling is still catching up, so adopt incrementally. New library? Try modules. Million-line codebase? Wait for your build system and CI to stabilize their support, then migrate leaf-first.
Related articles
- C++ C++ CMake Tutorial: Build Your First Project
Learn modern CMake for C++ — targets, properties, and dependencies — and configure a small project that compiles cleanly across platforms.
- C++ C++ constexpr and Compile-Time Computing
How constexpr, consteval, and constinit let you move computation from runtime to compile time, with practical patterns and the rules that govern them.
- C++ C++ Coroutines: An Introduction
Understand C++20 coroutines — co_await, co_yield, co_return — and how they enable async and generator-style code without blocking threads.
- C++ C++ Design Patterns Overview
Tour the most useful design patterns in modern C++ — singleton, factory, observer, strategy, RAII — and learn when each one earns its keep.