Skip to main content

Modules (C++20)

Modules are C++20's replacement for the textual #include model. Instead of the preprocessor pasting header text into every translation unit, a module is compiled once into a binary module interface (BMI) that consumers import. This fixes the header model's three chronic problems: slow builds, macro leakage, and fragile include order.

Prerequisite contrast

This page assumes the header model it replaces โ€” see Headers and Includes and Include Guards / pragma once. Modules make include guards obsolete and macros no longer leak across the boundary.

The problem modules solveโ€‹

With #include, every translation unit re-reads and re-parses the full text of every header it pulls in (transitively). A common header parsed across 200 source files is parsed 200 times.

A module in two filesโ€‹

Interface unit โ€” declares what the module exports:

// math.ixx (or .cppm โ€” extension is toolchain-specific)
export module math; // declares the module

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

int helper() { return 42; } // no 'export' = private to the module

Consumer โ€” imports it by name, no path, no guard:

import math; // not #include "math.h"

int main() { return add(2, 3); } // helper() is invisible here

What export controlsโ€‹

Only export-ed names are visible to importers; everything else is module-local โ€” true encapsulation the header model never had.

export module widget;

export class Widget { /* ... */ }; // part of the public interface
export void configure(Widget&);

namespace detail { // entire namespace stays internal
int compute();
}

Larger modules split into partitions (export module widget:parts;) and separate implementation units (module widget; with no export), keeping the interface small while the definitions live elsewhere โ€” analogous to the header/source split, but without re-parsing.

import std; (C++23)โ€‹

C++23 ships the entire standard library as a module. One import replaces dozens of headers and compiles dramatically faster:

import std; // the whole standard library

int main() {
std::vector<int> v{1, 2, 3};
std::println("{}", v.size());
}

Why it's better โ€” and the catchโ€‹

Benefits
  • Faster builds โ€” an interface is compiled once, not re-parsed per includer.
  • Isolation โ€” macros and private declarations do not leak across import.
  • No include guards / ordering โ€” import order is irrelevant; no ODR landmines from header soup.
Toolchain maturity

Modules need coordinated compiler and build-system support, because a module must be built before anything that imports it (a dependency the build tool has to discover). Support in GCC, Clang, MSVC, CMake, and Ninja is solid but still maturing. BMIs are not portable โ€” compiler/version specific โ€” so they are a build artifact, never something you ship or check in.

Summaryโ€‹

  • A module is compiled once into a BMI and imported โ€” replacing textual #include.
  • export defines the public interface; everything else is module-local (real encapsulation).
  • Macros and private names don't cross an import; include guards become unnecessary.
  • import std; (C++23) pulls in the whole standard library, fast.
  • The model is sound but depends on build-system support; BMIs are non-portable build artifacts.