C++ Modules (C++20)


  • Description: A note on C++20 modules — import, export, module partitions, header units, the global module fragment, and migration from #include
  • My Notion Note ID: K2A-B1-27
  • Created: 2022-03-15
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Why Modules?

  • #include pastes header text into every TU. Large project → same headers reparsed thousands of times; macros can leak.

Modules fix both:

  1. One-time compilation — module parsed once, imported as BMI (Binary Module Interface).
  2. Hard isolation — macros, internal symbols, unrelated decls don't leak across module boundary.
  3. Faster builds — no per-TU re-parse.
  4. Cleaner ABI control — only exported symbols visible to importers.
  • Not a runtime feature — changes build model, not generated code.

2. Importing a Module

import std;                  // C++23: the entire standard library as one module
import std.compat;           // C++23: same as std plus C library names in the global namespace
import math;                 // your own module
import :helpers;             // a module partition (only inside another module)
  • import looks like a statement, but must appear at top of file (after global module fragment if any).
  • Doesn't introduce names into current scope — only names exported by the module.

3. Defining a Module

  • Primary interface unit declares the module + what it exports:
// math.cppm                 (the .cppm extension is convention; not required)
export module math;          // primary interface

export int add(int a, int b) {     // exported, visible to importers
    return a + b;
}

int internal(int x) {              // NOT exported; internal to this TU
    return x * 2;
}

export {                            // export multiple declarations at once
    int sub(int a, int b);
    int mul(int a, int b);
}
  • Implementation files (no export on module decl) provide bodies:
// math_impl.cpp
module math;                       // implementation unit (no `export`)

int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

4. Module Partitions

  • Large modules split into partitions — submodules sharing the parent's name.
// math-trig.cppm
export module math:trig;           // partition of math

export double sin(double x);
export double cos(double x);

// math.cppm
export module math;
export import :trig;               // re-export the partition
  • Importers see one logical module:
import math;
math::sin(0.5);
  • Partitions can also be private (not re-exported):
// math.cppm
export module math;
import :impl_helpers;              // not re-exported; for use only inside math

5. Header Units

  • Import an existing header as if it were a module. Useful during migration.
import <vector>;          // standard header as a module
import "myheader.h";      // your header as a module
  • Header units preserve macros (named modules don't), but compile faster than #include.
  • Stepping stone, not final destination.

6. Global Module Fragment

  • Module needs #include-only header (un-modularized) → global module fragment at top:
module;                            // begin global module fragment
#include <vector>                   // legacy header
#include "legacy_macro.h"           // bring in macros, types, etc.

export module myapp;                // begin the module proper

import std;                         // can also use proper imports

export void foo(std::vector<int>);
  • Holdover for legacy interop. New code → prefer import and import <header>;.

7. Migration from #include

Practical migration path:

  1. Convert heaviest, most-included headers first (broad include-graph). Header units (import <header>;) → easy win.
  2. Modularize internal libraries as named modules. #include "internal.h"import internal;.
  3. Public APIs last — more visible interface + ABI.
  4. Don't mix lifestyles — once a header is a module, consumers should import.
  • Module-aware compilers consume #include for unported headers → no flag day needed.

8. Compiler and Build-System Support

As of late 2025:

  1. MSVC — strongest. Named modules with /std:c++20. import std; needs /std:c++latest.
  2. Clang — named modules solid since Clang 16; import std; since Clang 18 (libc++ configured to ship std module).
  3. GCC — improving; import std; + advanced features lag others.
  4. CMake — named-module support in 3.28 (CMAKE_CXX_SCAN_FOR_MODULES); import std; in 3.30.
  5. Bazel, Meson, etc. — separate module integrations; check vendor docs.
  • Reality check: real build-time wins, toolchain still maturing. Most codebases still on #include. Expect to mix both for a while.