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:

  1. Compilation Overhead: The same header files are parsed repeatedly across dozens or hundreds of translation units.
  2. 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 import statement 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_X before #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.