C++20 Modules Complete Guide: From Legacy Headers to Modern Builds
Master C++20 modules: replace headers, improve compile times, and enforce encapsulation.
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
- ✓Basic knowledge of C++ syntax and compilation model
- ✓Familiarity with header files and the preprocessor
- ✓A compiler that supports C++20 modules (GCC 11+, Clang 16+, MSVC 2022 17.0+)
- Modules replace header files with a more efficient, encapsulated import system.
- They drastically reduce compile times by eliminating transitive includes.
- Modules enforce better encapsulation: only exported entities are visible.
- They eliminate the need for include guards and reduce macro pollution.
- Modules are a core feature of C++20, supported by major compilers (GCC, Clang, MSVC).
Think of a library as a room with a door. Headers are like leaving the door wide open—anyone can see everything inside, and you have to check every visitor's bag. Modules are like a receptionist who only lets in the people you explicitly invite. This makes the library safer and faster to enter.
For decades, C++ developers have relied on the preprocessor and header files to share declarations across translation units. While functional, this approach has significant drawbacks: long compile times due to transitive includes, fragile dependency ordering, macro pollution, and the infamous ODR violations. C++20 introduces modules—a modern, scalable solution that addresses these pain points head-on. Modules provide a new way to organize source code into logical units with explicit export boundaries. They eliminate the need for include guards, reduce compilation overhead, and improve encapsulation. In this guide, you'll learn how to define, export, and import modules, how to partition large modules, and how to integrate modules into existing projects. We'll cover real-world debugging scenarios, common mistakes, and interview questions. By the end, you'll be ready to adopt modules in your next C++20 project.
What Are C++20 Modules?
Modules are a new way to organize C++ source code into self-contained units with explicit export boundaries. Unlike headers, which rely on textual inclusion and the preprocessor, modules are compiled once and their interface is imported by other translation units. This eliminates the need for include guards, reduces macro pollution, and significantly improves compile times. A module consists of one or more module units: a module interface unit (usually .cppm or .ixx) that declares what is exported, and module implementation units that provide definitions. The key syntax includes 'export module module_name;' to define a module, 'export' to make entities visible, and 'import module_name;' to use a module. Modules also support submodules and partitions for larger codebases.
Defining and Exporting a Module
To define a module, create a module interface unit with the extension .cppm (or .ixx on MSVC). The file must start with 'export module module_name;'. You can then export functions, classes, variables, and namespaces. Use the 'export' keyword before each declaration you want to expose. For example:
```cpp export module math_utils;
export int add(int a, int b) { return a + b; }
export class Calculator { public: int multiply(int a, int b); }; ```
You can also export a namespace: 'export namespace Math { ... }'. Entities not exported are module-private and cannot be accessed by importers. This enforces encapsulation. Module implementation units (with extension .cpp) can provide definitions for exported functions declared in the interface. They use 'module math_utils;' (without export) to indicate they belong to the module.
Importing Modules
To use a module, use the 'import' directive at the top of your source file (after any module declaration if you are also defining a module). For example:
```cpp import math_utils;
int main() { int sum = add(5, 3); return 0; } ```
You can import multiple modules: 'import module1; import module2;'. Modules can also import other modules, creating a dependency graph. Unlike headers, modules are compiled once and cached, so importing them is cheap. You can also import header units (legacy headers) using 'import <iostream>;' but this is less efficient. Note that import is not a preprocessor directive; it is a new keyword that works at the semantic level.
Module Partitions
For large modules, you can split them into partitions. A partition is a sub-unit of a module that can be internal or exported. Internal partitions are only visible within the module; exported partitions are visible to importers. Syntax:
Module interface partition (e.g., math_utils_add.cppm): ``cpp export module math_utils:add; export int add(int a, int b); ``
Primary module interface (math_utils.cppm): ``cpp export module math_utils; export import :add; // re-export the partition export import :subtract; ``
Implementation partitions provide definitions. Partitions help organize code without exposing internal structure. They are compiled separately but belong to the same module.
Modules vs Headers: A Comparison
- Compile time: Modules are compiled once and cached; headers are recompiled in every translation unit that includes them.
- Encapsulation: Modules only export what is explicitly marked; headers expose everything (unless you use pimpl or similar).
- Macro pollution: Modules do not leak macros; headers can cause macro conflicts.
- ODR: Modules enforce ODR more strictly; headers can lead to multiple definitions if not guarded.
- Dependency management: Modules have a clear dependency graph; headers rely on include order and guards.
However, modules are not a complete replacement yet. Some features like preprocessor macros are not available in modules, and legacy code may still require headers. The C++ committee is working on header units to ease migration.
Common Pitfalls and Best Practices
Common mistakes when using modules: 1. Forgetting to export: If you forget 'export', the entity is module-private and not visible to importers. 2. Circular imports: Modules cannot import each other directly; use partitions or restructure. 3. Mixing modules and headers: Be careful when a module imports a header that defines macros; the macros are not visible in the module. 4. Incorrect file extensions: Some compilers require specific extensions (.cppm, .ixx). Check your compiler documentation. 5. Not compiling the module interface first: The module interface unit must be compiled before any file that imports it.
- Use a consistent naming convention for modules (e.g., reverse domain).
- Keep module interfaces small and focused.
- Prefer exporting entire namespaces over individual declarations.
- Use module partitions to organize large modules.
- Measure compile times to validate improvements.
The 3-Hour Build That Became 20 Minutes
- Transitive includes are a major source of compile-time bloat.
- Modules provide a clean boundary that breaks dependency chains.
- Adopting modules incrementally is possible; start with stable, low-level libraries.
- Measure build times before and after to quantify improvements.
- Educate the team on module semantics to avoid common pitfalls.
g++ -std=c++20 -fmodules-ts -c mymodule.cppmg++ -std=c++20 -fmodules-ts main.cpp mymodule.o| File | Command / Code | Purpose |
|---|---|---|
| hello.cppm | export module hello; | What Are C++20 Modules? |
| math_utils.cppm | export module math_utils; | Defining and Exporting a Module |
| main.cpp | int main() { | Importing Modules |
| bad_example.cppm | module mymodule; | Common Pitfalls and Best Practices |
Key takeaways
Common mistakes to avoid
5 patternsForgetting to export a function or class
Using #include inside a module interface
Not compiling the module interface before importing it
Assuming modules are filesystem paths
Circular imports between modules
Interview Questions on This Topic
What are the main advantages of C++20 modules over traditional headers?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
That's C++ Advanced. Mark it forged?
3 min read · try the examples if you haven't