C++ Header Units: Bridging Headers and Modules
Header units in modern C++ provide a standardized migration path from
legacy preprocessor #include directives to modular
compilation architectures. Introduced in C++20, they allow developers to
import traditional C++ header files as if they were modules, delivering
benefits such as macro isolation, faster build times via precompilation,
and reduced translation-unit pollution. This article explains how header
units function, their syntactic differences from standard modules, how
they handle macros, and their role in transitioning existing codebases
to modern C++ module workflows.
The Architectural
Divide: #include vs. import
Traditional C++ compilation relies on #include, a purely
textual preprocessor mechanism. When a file includes a header, the
preprocessor copies the entire contents of that header into the
translation unit. This approach suffers from two major drawbacks:
- Compilation Overhead: The same header files are parsed repeatedly across dozens or hundreds of translation units.
- Leaky Encapsulation: Macros and declarations in a header pollute downstream code, and pre-existing macros can inadvertently alter the behavior of subsequent headers.
C++20 modules resolve these issues by compiling code into distinct
Binary Module Interfaces (BMIs) and consuming them via
import module_name;. However, rewriting large legacy
codebases and third-party libraries entirely into native C++20 modules
is impractical for most organizations.
What Are Header Units?
Header units serve as a bridge between textual inclusion and module-based imports. A header unit is a separate, compiled translation unit synthesized on-demand from an existing header file. Instead of textually including a header, you import it using module syntax:
// Traditional include
#include <vector>
#include "my_header.h"
// Header unit import
import <vector>;
import "my_header.h";When the compiler encounters import <vector>;, it
does not perform textual expansion. Instead, it reads an
already-compiled binary representation of <vector>,
significantly accelerating compile times.
How Header Units Bridge the Gap
1. Macro Hygiene and Isolation
Header units strike a careful balance between the isolation of modules and the realities of legacy header designs:
- Inward Isolation: Macros defined in the importing
translation unit prior to the
importstatement do not affect the internal code of the header unit. The header unit compiles independently. - Outward Export: Unlike standard named modules (which hide all internal macros), header units export object-like and function-like macros defined within them. This ensures that legacy libraries relying on API-level configuration macros (such as feature flags or version macros) continue to work without code modification.
2. Binary Module Interface Generation
When a header unit is declared, modern build tools compile the header file into a Binary Module Interface (BMI), similar to a standard module. Subsequent translation units importing the same header unit reuse the generated BMI. This preserves the performance gains of modules while retaining the file structure of legacy headers.
3. Preprocessor
Translation (Include Translation)
Compilers that implement C++20 module features can be configured to
automatically translate #include directives into header
unit imports when a matching header unit is detected:
// If <iostream> is built as a header unit, the compiler can treat:
#include <iostream>
// Internally as:
import <iostream>;This feature allows teams to gain the performance advantages of header units without modifying a single line of legacy source code.
Implementation Requirements and Constraints
Not every header file can become a header unit without verification:
- Self-Containment: A header unit must be capable of compiling on its own. Headers that depend on other headers being included beforehand without explicitly importing or including them will fail.
- Macro Configuration Dependencies: If a header
relies on specific preprocessor directives set by the consumer prior to
inclusion (such as defining
#define USE_FEATURE_Xbefore#include "library.h"), it cannot be used as a header unit in that configuration, as the imported unit compiles in isolation. - Toolchain Alignment: Build systems such as CMake, MSVC, Clang, and GCC require explicit dependency tracking to construct the directed acyclic graph (DAG) of header unit dependencies before building downstream consumers.
Summary
Header units provide an incremental adoption model for modern C++. By treating traditional headers as importable binary artifacts rather than raw text, they enable teams to capture the compile-time advantages and hygiene of modules while preserving compatibility with vast ecosystems of pre-existing C++ code.