Modules

C++20 modules replace decades of copy-pasting header text into every translation unit with a compiled interface you import directly — faster builds, no macro leakage, and real encapsulation that #include never had.

Every #include directive does the same primitive thing: it textually pastes the contents of a file into whatever source file included it, before the compiler ever sees a single token of real C++. That model has powered C++ since the beginning, but it comes with costs that compound as codebases grow — headers get reparsed from scratch in every translation unit that includes them, macros defined in one header leak into everything included after it with no clean way to scope them, and subtle bugs can depend on include order. C++20 modules replace textual inclusion with a genuinely new compilation unit: a module is compiled once into a binary interface, and consuming code imports that interface directly instead of re-parsing source text. This page works through what specifically was wrong with headers, how a module interface and its implementation are structured, what import and export actually control, and what changes about how a project builds once modules are in the mix.

The Problem With Headers

Textual inclusion means a header's entire contents are reparsed, from a token-by-token blank slate, in every single translation unit that includes it. A heavily included header — one used by a hundred .cpp files — gets parsed by the compiler a hundred times, and every declaration, every template, every inline function inside it is redone that many times, even though the result never changes.

The include guard (#ifndef/#define/#endif) exists only to work around a problem headers create for themselves — including the same header twice in one translation unit, directly or through two other headers that both include it, would otherwise redefine Widget and fail to compile.

Macros compound the problem, because #define has no scope at all — a macro defined in one header stays active in every line of every file included afterward, in the same translation unit, until an explicit #undef (if anyone remembers to write one). A header that defines #define max(a, b) ... can silently break code far away that was calling std::max or a member function named max, with an error message that gives no hint the culprit was a macro from an unrelated header pulled in three includes earlier.

Include order matters too, in ways that are easy to get wrong: if header A depends on a type declared in header B but doesn't include B itself, code that happens to include B before A keeps compiling by accident, while code that includes only A breaks — a bug that surfaces only when someone reorders includes.

// widget.h
#ifndef WIDGET_H
#define WIDGET_H

class Widget
{
public:
    void render();
};

#endif // WIDGET_H

Modules: A Different Compilation Model

A module is declared with export module followed by the module's name, in a module interface unit — conventionally given a .cppm (or .ixx, depending on the compiler) file extension. Entities marked export inside it become visible to anyone who imports the module; everything else stays private to the module. Consuming code uses import ModuleName; instead of #include "module_name.h".

export module math_ops; declares this file as the interface for a module named math_ops. Both add and multiply are marked export, so both become visible to anything that imports math_ops.

import math_ops; brings add and multiply into scope. Notice this is not textual inclusion — the compiler consumes a pre-analyzed representation of the module's interface rather than reparsing math_ops.cppm's source text, and import statements, unlike #include, can appear in any order relative to each other without changing meaning.

// math_ops.cppm — module interface unit
export module math_ops;

export int add(int a, int b)
{
    return a + b;
}

export int multiply(int a, int b)
{
    return a * b;
}
// main.cpp
import math_ops;
#include <iostream>

int main()
{
    std::cout << add(2, 3) << "\n";
    std::cout << multiply(2, 3) << "\n";
    return 0;
}

Interface Units vs Implementation Units

A module can be split across more than one file. The interface unit (export module Name;) declares the module and everything meant to be visible outside it. A module implementation unit (module Name; — without export) belongs to the same module but is not itself part of the interface; it can define additional internal helper functions, or provide out-of-line bodies for functions merely declared in the interface, none of which becomes visible to importers.

module math_ops; (with no export keyword) attaches this file to the math_ops module without adding to its exported interface. Splitting declarations from definitions this way mirrors the old .h/.cpp split, but here the split is enforced by the module system rather than by convention.

// math_ops.cppm — interface: declares the public API
export module math_ops;

export int add(int a, int b);
export int multiply(int a, int b);
// math_ops_impl.cpp — implementation unit: defines the bodies
module math_ops;

int add(int a, int b)
{
    return a + b;
}

int multiply(int a, int b)
{
    return a * b;
}

export — What Crosses the Boundary

Anything declared inside a module without export is invisible outside it, even though it lives in the same source file as exported declarations. This is real encapsulation that headers never provided: a header hides nothing from its includers, since including a header always exposes everything written in it, but a module can keep helper functions, internal types, and implementation details completely private to itself.

distance can use square internally, but code that writes import geometry; has no way to call square directly — it simply is not part of what the module exports, the way a private member function in a class is inaccessible outside that class, but enforced at the level of a whole translation unit instead of a class.

export can also apply to a whole block: export { ... } marks every declaration inside the braces as exported, which is convenient when a module exports many related names at once.

Why does a heavily included header get reparsed far more times than a module with equivalent content gets recompiled?

Why can a macro defined in one header silently break unrelated code much later in the same translation unit, and how do modules avoid that specific problem?

What is the difference between a module interface unit and a module implementation unit, and what determines which one a given file is?

A module interface unit is declared with export module Name; and holds everything (declared with export) that becomes visible to code that imports the module. A module implementation unit is declared with module Name; (no export) and belongs to the same module, but nothing in it becomes part of the module's exported interface — it can provide extra internal definitions or the out-of-line bodies of things only declared in the interface unit.

How does export inside a module differ from simply putting a declaration in a header file?

Everything written in a header becomes visible to every file that includes it — headers provide no way to hide implementation details from includers. A module can declare entities without export, keeping them fully private to the module; only entities explicitly marked export (or inside an export { } block) become visible to code that imports the module, giving genuine encapsulation at the translation-unit level.

Write a small C++20 module named strings_utils that exports a single function to_upper(std::string) returning the uppercased string, plus one non-exported internal helper it uses, and a main.cpp that imports and uses it.

// strings_utils.cppm export module strings_utils; #include <string> #include <cctype> // Internal helper: not exported, invisible outside this module. char to_upper_char(char c) { return static_cast<char>(std::toupper(static_cast<unsigned char>(c))); } export std::string to_upper(std::string text) { for (char& c: text) c = to_upper_char(c); return text; } // main.cpp import strings_utils; #include <iostream> int main() { std::cout << to_upper("hello, modules") << "\n"; return 0; } to_upper_char is a plain function defined inside the module with no export keyword, so it is only callable from other code inside strings_utils — main.cpp has no way to call to_upper_char directly even though it imports the module. to_upper is marked export, so importing strings_utils makes it callable from main.cpp, which prints "HELLO, MODULES".

Write a C++20 module split into an interface unit and a separate implementation unit for a module named counter that exports a function next_id() returning successive integers starting at 1 each time it is called, and a main.cpp that calls it three times.

// counter.cppm — interface unit export module counter; export int next_id(); // counter_impl.cpp — implementation unit module counter; int next_id() { static int current = 0; return ++current; } // main.cpp import counter; #include <iostream> int main() { std::cout << next_id() << " " << next_id() << " " << next_id() << "\n"; return 0; } counter.cppm declares next_id() as part of the module's exported interface but does not define its body. counter_impl.cpp attaches to the same module with module counter; (no export) and supplies the actual definition, including the internal static counter state, none of which is visible from outside the module. main.cpp only ever sees the declaration exported from the interface unit, and the three calls print "1 2 3".

export module geometry;

// Not exported: invisible to anything that imports geometry.
double square(double x)
{
    return x * x;
}

// Exported: part of the public interface.
export double distance(double x1, double y1, double x2, double y2)
{
    return std::sqrt(square(x2 - x1) + square(y2 - y1));
}