Header Units
The transition from a text-based inclusion model (#include) to a semantic module model (import) Cannot happen instantaneously. The C++ ecosystem relies on millions of lines of headers (POSIX, Windows SDK, OpenSSL, Boost) that may never be converted to native modules.
To bridge this gap, C++20/23 introduces two architectural mechanisms:
- The Global Module Fragment (GMF): A reliable sandbox for
#includedirectives within module files. - Header Units: A mechanism to compile legacy headers into Binary Module Interfaces (BMIs) on the fly, allowing them to be imported like modules.
1. What Header Units Solve
Section titled “1. What Header Units Solve”In the traditional #include model, every translation unit that includes a header re-parses the Entire header text. For large headers like <vector> or <windows.h>This can mean Parsing tens of thousands of lines for every TU. The preprocessor performs textual inclusion, macro Expansion, and conditional compilation independently for each TU.
Header units address this redundancy by compiling the header once into a BMI, which is then Imported by all consumers. This is analogous to precompiled headers (PCH) but with semantic Guarantees:
- The header is parsed only once per build.
- The BMI captures the semantic interface (declarations, types, templates).
- Macro behavior is preserved (unlike native modules).
Formal Basis
Section titled “Formal Basis”The C++ Standard defines a header unit as a synthesized module unit whose module-name is the header File’s textual representation [N4950 S15.2]. When the compiler encounters import "foo.h";It Constructs a module unit conceptually equivalent to:
module;#include "foo.h"export module "foo.h";export // all top-level declarations from foo.hThis formalization explains two critical behaviors: (1) macros defined by the header are visible to The importer, because they are present before the export module boundary; and (2) all declarations Are exported, because the export keyword applies to the translation unit’s interface.
Proof: Header Units Eliminate Redundant Parsing
Section titled “Proof: Header Units Eliminate Redundant Parsing”Consider a project with TUs Each including header . Let denote The cost of parsing and the cost of serializing its semantic interface.
#include model: Each TU independently preprocesses and parses . Total cost:
C_{\mathrm{include} = n \cdot (P(H) + S(H))
Header unit model: is parsed once, producing a BMI. Each TU deserializes the BMI. Let Denote deserialization cost, where because deserialization reads a pre-structured AST rather than re-parsing raw text. Total cost:
C_{\mathrm{header\_unit} = P(H) + S(H) + n \cdot D(H)
The savings are:
C_{\mathrm{include} - C_{\mathrm{header\_unit} = (n - 1)(P(H) + S(H) - D(H))
Since for any non-trivial header, the savings grow linearly with the number of TUs. For a header like <vector> with P(H) \approx 50\mathrm{ms and D(H) \approx 5\mathrm{ms Across 100 TUs, the savings are approximately seconds per incremental build.
2. The Global Module Fragment (GMF)
Section titled “2. The Global Module Fragment (GMF)”The GMF is the primary mechanism for consuming legacy headers inside a module. It creates a Transitional environment where the preprocessor functions normally, but the resulting declarations Are seamlessly available to the module body.
Architecture
Section titled “Architecture”The GMF is delimited by the module; statement at the very top of the file and ends at the export module [Name]; declaration.
module; // START GMF
// Legacy inclusions happen here.// The preprocessor runs typically, handling macros and text substitution.#include <sys/socket.h>#include <openssl/ssl.h>
export module Network; // END GMF
// From this point forward, the preprocessor state is isolated.// Macros defined in the headers above ARE visible here.// Macros defined below ARE NOT visible to importers of 'Network'.
import std;
export namespace Network { void init_socket() { // We can use sockaddr_in from sys/socket.h sockaddr_in addr{}; }}Formal Basis
Section titled “Formal Basis”Per [N4950 S15.1], the global module fragment is the region of a module unit that precedes the Module-declaration. The key semantic property is that names introduced by preprocessing directives (including #include) in the global module fragment are attached to the global module and are not owned by the named module being declared.
This means the declarations are reachable within the module implementation, but they are not exported to importers of the named module [N4950 S15.5.2]. The standard distinguishes between “exported” names (visible to importers) and “reachable” names (visible for name lookup within the Module but not part of its interface).
Isolation Rules
Section titled “Isolation Rules”- Leakage: Declarations in the GMF (like
sockaddr_in) are reachable by the module implementation, but they are not exported to consumers ofNetworkunless explicitly re-exported. - Macro Hygiene: Macros defined in the GMF are visible inside the module. However, when a consumer writes
import Network;They do not inherit these macros. This prevents the “macro pollution” common in header-based development.
Proof: GMF Prevents Macro Pollution
Section titled “Proof: GMF Prevents Macro Pollution”Consider a header platform.h that defines #define MAX_CONNECTIONS 100. Two scenarios:
Scenario A — #include in every TU: Every TU that includes platform.h or any header that Transitively includes it acquires the MAX_CONNECTIONS macro. This macro can interfere with Unrelated code in any TU that happens to define a local variable named MAX_CONNECTIONSCausing Silent redefinition.
Scenario B — GMF in module Network: The macro MAX_CONNECTIONS is visible inside network.cppm but is not propagated to any TU that writes import Network;. The standard Guarantees this because the export module declaration terminates the preprocessing scope of the GMF [N4950 S15.1 p5], and macros are not entities that participate in module export/import semantics (except in header units, discussed in Section 7).
3. Importing a Header Unit vs Including It
Section titled “3. Importing a Header Unit vs Including It”Syntax
Section titled “Syntax”Instead of #include "header.h"The syntax is import "header.h"; (or import <header>;).
import <vector>; // Header Unit importimport "my_lib.h";
int main() { std::vector<int> v; // Works}Key Differences from #include
Section titled “Key Differences from #include”| Feature | #include "h" | import "h" (Header Unit) |
|---|---|---|
| Parsing | Textual copy-paste. | Semantic load (BMI). |
| Speed | Slow (re-parsed every TU). | Fast (parsed once per build graph). |
| Macros | Leaks everywhere. | Leaks. (Unique exception). |
| ODR | Fragile. | Strict. |
| Include guards | Required for safety | Not needed (no textual inclusion). |
| Preprocessing | Full preprocessing applied | Single precompile pass. |
| Re-export | Not applicable | Possible via export import |
| Include order | Matters (textual) | Matters (macro state dependent) |
The Synthesis Mechanism
Section titled “The Synthesis Mechanism”When the build system encounters import "header.h";:
- It scans
header.h. - It compiles
header.hinto a BMI (.pcm/.ifc). - It replaces the text inclusion with a binary load of the interface.
This means the header is processed once by the preprocessor and once by the compiler’s semantic Analysis. All subsequent importers load the pre-built BMI, skipping both steps.
Formal Semantic Difference
Section titled “Formal Semantic Difference”Per [N4950 S15.2], a header-unit import is a module import, not a preprocessing directive. This Means:
- The header unit is processed as a separate translation unit [N4950 S5.2].
- It contributes declarations to the importing TU’s scope as if the declarations were declared in the global module fragment.
- The
importdirective is a module-import-declaration [N4950 S7.6.5], which obeys module visibility rules (the imported names are exported to the importing TU).
The critical distinction: #include performs textual substitution (preprocessing), while import performs semantic import (name binding). The latter can be type-checked, ordered, and Cached independently.
Proof: Header Units Enforce Stricter ODR
Section titled “Proof: Header Units Enforce Stricter ODR”Consider a header widget.h containing:
struct Widget { int x; double y;};With #includeIf TU-A includes widget.h with #define EXTRA_FIELD before the include, and TU-B Includes it without that define, the two TUs may see different definitions of Widget. This Violates the ODR [N4950 S6.3], but the violation is undetectable at compile time because each TU Compiles independently.
With header units, the header is compiled once with a specific preprocessor state. All importers See the same definition (the one baked into the BMI). If a different TU needs a different Preprocessor configuration, it requires a different BMI, which the build system must track. This Makes ODR violations detectable at the build system level (mismatched BMIs) rather than Remaining latent until runtime.
4. Header Unit Naming and File System Layout
Section titled “4. Header Unit Naming and File System Layout”Header units use the header’s path as their identity:
import <vector>;refers to the standard library header namedvector.import "utils/helpers.h";refers to the fileutils/helpers.hrelative to the include paths.
The build system must locate the header using the same search rules as #include (system include Paths for <>Then user include paths for "").
Naming in the BMI
Section titled “Naming in the BMI”The BMI for a header unit is named after the header path:
# Clang generates BMIs with a path-based naming schemeclang++ -std=c++23 --precompile header_unit.hpp -o header_unit.hpp.pcm
# MSVC uses an .ifc file alongside the headerClang Command Line
Section titled “Clang Command Line”# Compile a header unitclang++ -std=c++23 -c --precompile utils/helpers.h -o utils/helpers.h.pcm
# Import the header unitclang++ -std=c++23 -c main.cpp -fmodule-file=utils/helpers.h=utils/helpers.h.pcmGCC Command Line
Section titled “GCC Command Line”# GCC uses -fmodule-header flagg++ -std=c++23 -fmodules-ts -fmodule-header utils/helpers.h -o utils/helpers.h.gcmMSVC Command Line
Section titled “MSVC Command Line”# MSVC uses /exportHeader (or /headerUnit with /ifcOutput)cl.exe /std:c++23 /exportHeader /headerUnit:angle utils\helpers.h /ifcOutput utils\helpers.h.ifc
# Consumercl.exe /std:c++23 /headerUnit:angle utils\helpers.h=utils\helpers.h.ifc main.cpp5. GCC vs. Clang vs. MSVC Implementation Differences
Section titled “5. GCC vs. Clang vs. MSVC Implementation Differences”Clang treats header units as a distinct BMI type. The --precompile flag with a header file Generates a header-unit BMI. Clang tracks whether a BMI is a named module or a header unit Internally.
# Clang: explicit header unit compilationclang++ -std=c++23 --precompile -x c++-system-header vector -o vector.pcmclang++ -std=c++23 --precompile -x c++-header my_lib.h -o my_lib.h.pcmClang requires specifying the header type (-x c++-header for user headers, -x c++-system-header For system headers) when precompiling header units. The -fmodule-file flag maps header paths to BMI files for consumers.
GCC uses the -fmodule-header flag to indicate that an input file should be compiled as a header Unit rather than a module interface. GCC’s header unit support is less mature than Clang’s.
# GCC: header unit compilationg++ -std=c++23 -fmodules-ts -fmodule-header my_lib.h# Generates a .gcm BMI that can be importedGCC relies on the module mapper to resolve header unit imports at compile time. The mapper Associates header paths with their corresponding BMI files.
MSVC uses /exportHeader to compile a header as a header unit, producing a .ifc file. The /headerUnit flag specifies the mapping from header names to .ifc files for consumers.
# MSVC: header unit compilationcl.exe /std:c++23 /exportHeader /headerUnit:angle utils\helpers.h /ifcOutput utils\helpers.h.ifc
# Consumer references the header unitcl.exe /std:c++23 /headerUnit:angle utils\helpers.h=utils\helpers.h.ifc main.cppMSVC integrates header unit scanning into its built-in dependency scanner, which CMake consumes via The P1689 protocol.
6. Interaction with Traditional Headers
Section titled “6. Interaction with Traditional Headers”Header units can coexist with traditional #include directives in the same project, but mixing them Requires care:
// Mixing is allowed but discouraged#include <cstdio> // Traditional includeimport <vector>; // Header unit import#include "legacy.h"; // Traditional includeimport "modern.h"; // Header unit importProblems with mixing:
- Macro conflicts: A macro defined by
#include <cstdio>may affect the behavior ofimport "modern.h";ifmodern.huses conditional compilation based on that macro. - ODR violations: If
legacy.handmodern.hboth define the same entity, the ODR is violated. The header unit model enforces ODR more strictly than textual inclusion. - Build system complexity: The build system must track both
#includedependencies andimportdependencies, using different scanning mechanisms for each.
Best practice: Pick one approach per header. Either #include it everywhere or import it Everywhere. Do not mix for the same header across different TUs.
The Preprocessor State Interaction Problem
Section titled “The Preprocessor State Interaction Problem”When #include and import are mixed in the same TU, the preprocessor state established by #include directives before an import directive can affect the header unit’s behavior during BMI Generation but not during BMI consumption. Consider:
#define CUSTOM_ALLOC 1#include "alloc.h" // alloc.h uses CUSTOM_ALLOCimport "data.h"; // data.h was compiled WITHOUT CUSTOM_ALLOCThe import "data.h" loads a pre-built BMI that was compiled without CUSTOM_ALLOC in scope. If data.h conditionally includes alloc.h or depends on CUSTOM_ALLOCThe imported version may Differ from what a textual #include would produce. This discrepancy is silent and dangerous.
7. Macro Export in Header Units
Section titled “7. Macro Export in Header Units”Header units are the only modular construct that exports macros. This is a deliberate Compatibility decision mandated by the standard’s treatment of header units as synthesized module Units [N4950 S15.2].
#define MAX_SIZE 100#define DEBUG_MODE 1import "config.h"; // Header unit import
int buf[MAX_SIZE]; // Works: MAX_SIZE is visible#if DEBUG_MODE // Works: DEBUG_MODE is visible// ...#endifThis means:
- Named modules (
export module Foo;) do not export macros. Importers do not see macros defined in the module interface. - Header units (
import "config.h";) do export macros. Importers see all macros defined by the header.
This asymmetry exists because many headers (especially configuration headers) are designed to Communicate exclusively through macros. Making them invisible would break the entire ecosystem.
Proof: Why Named Modules Cannot Export Macros
Section titled “Proof: Why Named Modules Cannot Export Macros”Macros are not part of the C++ type system. They are preprocessing tokens [N4950 S6.10] that are Expanded before the compiler performs semantic analysis. The module system operates at the semantic Level — it exports declarations (entities with names, types, and linkage). Macros have no type, No linkage, and no namespace qualification. Therefore, the module export mechanism [N4950 S15.5.2] Has no way to represent them.
Header units are a special case because they are defined as a synthesis of #include and module Semantics [N4950 S15.2]. The standard specifies that the preprocessor state (including macros) from The header is made available to importers as a concession to compatibility.
Implications:
- Header units that define macros still suffer from macro pollution in importers.
- You cannot use header units to achieve macro hygiene. Use the Global Module Fragment with named modules for that.
- If a header’s sole purpose is to define macros, it is a poor candidate for header unit conversion. Keep using
#includeor move the macros toconstexprvariables in a named module.
8. Current Limitations and Edge Cases
Section titled “8. Current Limitations and Edge Cases”Not All Headers Are Importable
Section titled “Not All Headers Are Importable”A header must be self-contained and idempotent to be used as a header unit:
- Self-contained: It must not depend on macros defined by the includer before the include directive.
- Idempotent: It must have include guards or
#pragma once.
Headers that violate these rules will compile differently as header units than as includes, causing Subtle bugs.
// BAD: not self-contained// User must define PLATFORM before including#ifdef PLATFORM_LINUXvoid linux_specific();#elif defined(PLATFORM_WINDOWS)void windows_specific();#endifOrder-Dependent Includes
Section titled “Order-Dependent Includes”Headers that must be included in a specific order (common in older C APIs) cannot be reliably Converted to header units:
// BAD: order matters#include <sys/types.h> // Must be before sys/socket.h#include <sys/socket.h>With #includeThe textual inclusion order is preserved. With importEach header unit is Compiled independently, and the order of import directives may not replicate the textual order Semantics.
Conditional Compilation
Section titled “Conditional Compilation”Headers that rely heavily on preprocessor conditionals are problematic as header units. The BMI Captures one specific configuration. If different TUs need different configurations of the same Header, header units cannot accommodate this.
// config.h defines different things based on BUILD_VARIANT// If TUs A and B need different BUILD_VARIANT values, they cannot// share the same header unit BMI.System Headers
Section titled “System Headers”Standard library header units (import <vector>;) are supported by Clang and MSVC but the Experience varies. GCC’s support is experimental. In C++23, import std; is the preferred approach For the standard library, making individual STL header units obsolete.
Third-Party Headers
Section titled “Third-Party Headers”Complex third-party headers (Qt, OpenSSL, Boost) are generally not suitable for header unit Conversion unless the library explicitly supports it. Use the Global Module Fragment instead.
9. Architectural Migration Strategy
Section titled “9. Architectural Migration Strategy”When modernizing a codebase to C++23 modules:
- Step 1: GMF Adoption. Move all
#includedirectives in.cppfiles to the Global Module Fragment of a new module. This validates that your headers are self-contained. - Step 2: Modularize Internals. Convert your own project’s components to Modules.
- Step 3:
import std;. Replace standard library includes with the named module. - Avoid Header Units. Use Header Units (
import "foo.h") only for headers you control that cannot be refactored into modules yet. For system headers, stick to the GMF mechanism.
Common Pitfalls
Section titled “Common Pitfalls”- Attempting to header-unit non-self-contained headers: If a header expects the user to
#definesomething before including it, it cannot be a header unit. Use#includein the GMF instead. - Macro-only headers: Headers that define only macros (no declarations) are poor candidates for header units. The BMI provides no semantic benefit over
#include. - ODR violations with mixed usage: Including a header in one TU and importing it as a header unit in another TU can violate the ODR if the preprocessor state differs. The build system may not detect this because the two compilation paths use different dependency tracking mechanisms.
- Build system not configured for header units: CMake requires explicit configuration (
CMAKE_CXX_SCAN_FOR_MODULES ON) and compiler-specific flags for header unit support. - Assuming header units provide macro hygiene: Unlike named modules, header units export macros. If macro isolation is the goal, use the GMF with a named module instead.
- Forgetting that header unit BMI identity includes preprocessor state: Two BMIs for the same header compiled with different
-Dflags are distinct. The build system must track this distinction or produce subtle link errors.
10. BMI Caching and Reproducibility
Section titled “10. BMI Caching and Reproducibility”Header unit BMIs are subject to the same caching and invalidation rules as named module BMIs. The Cache key includes the compiler version, flags (-std=c++23``-D...), and the full transitive Include chain. However, header unit BMIs have an additional complication: preprocessor state Matters.
Unlike named modules, which are macro-free (macros from the GMF are not exported), header units must Preserve macro definitions. This means two header unit BMIs compiled with different -D flags are not interchangeable, even if the source file is identical. This is a fundamental difference from Named module BMIs and a primary reason why header units are harder to cache reliably.
The build system must associate each header unit BMI with the exact set of preprocessor defines used During its compilation. If a TU compiled with -DDEBUG=1 imports a header unit that was compiled Without that flag, the resulting BMI will not contain the debug-guarded code paths, leading to Missing symbols at link time. Consider:
# These produce DIFFERENT BMIs for config.hclang++ -std=c++23 --precompile -DDEBUG=1 -x c++-header config.h -o config_debug.h.pcmclang++ -std=c++23 --precompile -x c++-header config.h -o config_release.h.pcmIf a TU compiled with -DDEBUG=1 imports config.hThe build system must locate and load the config_debug.h.pcm BMI, not the release variant. Build systems like CMake track this through the P1689 scanning protocol, but the dependency scanner must be aware of the compile flags to select The correct BMI. This is why header units increase build system complexity significantly compared to Named modules.
11. Header Units and #pragma once Interaction
Section titled “11. Header Units and #pragma once Interaction”#pragma once is a compiler extension that prevents multiple inclusion of the same header. When a Header is compiled as a header unit, the compiler processes it exactly once to produce the BMI, so Include guards and #pragma once are technically unnecessary for the header unit itself.
However, a header may be both #includeD and importEd across different TUs in the same Project. If the header lacks include guards and is #includeD transitively by another header that Is imported as a header unit, the preprocessor may process the text multiple times during BMI Generation. Therefore, include guards remain a best practice even for headers intended to be used as Header units.
12. Diagnostic Experience
Section titled “12. Diagnostic Experience”Error messages originating from header unit code are notoriously difficult to interpret. Because the Compiler operates on the serialized BMI rather than the original source text, error locations may Reference internal BMI offsets rather than source file line numbers. Clang has improved this Significantly in recent versions by embedding source location metadata in .pcm files, but GCC and MSVC still produce cryptic diagnostics for errors in imported header units.
Mitigation: Keep header unit headers small and well-tested. If a header is complex enough that Diagnostics matter, prefer converting it to a named module where the compiler can provide full Source-level error reporting.
13. Header Units in the Standard Library Context
Section titled “13. Header Units in the Standard Library Context”C++23 introduces the import std; named module as the preferred way to consume the standard library [P2465R3]. This raises the question of how individual standard library header units relate to the Named module.
import <header> vs import std;
Section titled “import <header> vs import std;”| Aspect | import <vector> | import std; |
|---|---|---|
| Scope | Single header | All standard library headers |
| BMI count | One BMI per imported header | One BMI for the entire standard library |
| Macro visibility | Yes (header unit semantics) | No (named module semantics) |
| Implementation | Compiler-specific | Compiler + stdlib collaboration |
| Performance | Good (per-header caching) | Better (single large BMI, loaded once) |
The import std; approach is strictly superior for new code because it provides macro hygiene (no Macro leakage) and a single BMI to cache. Individual header unit imports (import <vector>) Remain relevant during incremental migration or when a project needs to mix header-unit and Named-module imports.
14. export import for Header Units
Section titled “14. export import for Header Units”A named module can re-export a header unit, making its declarations available to downstream Importers:
module;#include <pthread.h>export module Platform;export import <pthread.h>; // Re-export the header unitConsumers of Platform gain access to the pthread declarations without needing their own import <pthread.h>. This is the standard’s mechanism for wrapping legacy headers in a module Interface [N4950 S15.5.2].
Note that the export import propagates the header unit’s macro definitions to the importer. If Macro hygiene is desired, the header must be included only in the GMF (without export import), and The declarations must be re-exported manually.
15. Header Units and the P1689 Scanning Protocol
Section titled “15. Header Units and the P1689 Scanning Protocol”Build systems that support header units must discover header unit imports during the dependency Scanning phase. The P1689R5 protocol defines how scanners report header unit dependencies:
{ "version": 1, "rules": [ { "primary-output": "CMakeFiles/App.dir/main.cpp.o", "depends": [ { "type": "module", "name": "Engine" }, { "type": "header-unit", "unique-on-source-path": true, "path": "utils/helpers.h" }, { "type": "header-unit", "unique-on-source-path": true, "path": "<vector>" } ] } ]}The scanner distinguishes between named module imports (type: "module") and header unit imports (type: "header-unit"). For header units, the build system must:
- Compile the header into a BMI before any TU that imports it.
- Pass the BMI path to the consumer via compiler-specific flags (
-fmodule-file``/headerUnit). - Track the header unit BMI as a build edge in the dependency graph.
This additional complexity is why header unit support requires build system integration (CMake 3.28+) and cannot be achieved with manual compiler invocations alone.
16. Header Units vs Precompiled Headers
Section titled “16. Header Units vs Precompiled Headers”Header units are often compared to Precompiled Headers (PCH), but they serve fundamentally different Purposes:
| Aspect | PCH | Header Unit |
|---|---|---|
| Scope | Arbitrary set of headers, order matters | Single header |
| Macro state | Captures all macros at PCH creation point | Captures macros defined by the header |
| Reusability | One PCH per TU (not shareable across TUs) | One BMI shared across all importers |
| Semantic guarantees | None (textual inclusion) | ODR enforcement via BMI |
| Build system awareness | Manual (compiler flag) | Automatic (P1689 scanning) |
| Module integration | Cannot be imported via import | Importable via import "h" |
| Macro leakage | Total (all macros leak) | Leaks header’s macros only |
The key architectural difference is that PCH is a performance optimization for the existing #include model, while header units are a semantic construct that participates in the module System. PCH does not change how the preprocessor works; header units do.
PCH remains useful for scenarios where you need to precompile a large set of headers that are always Included together (e.g., a “unity” PCH containing all STL headers). Header units are better when you Need modular semantics (ODR enforcement, explicit imports) for individual headers.
17. Transitioning from #include to import for a Single Header
Section titled “17. Transitioning from #include to import for a Single Header”When transitioning a single header from #include to a header unit import, follow this checklist:
- Verify self-containment: The header must not depend on macros defined by the includer. Test by compiling the header as a standalone translation unit.
- Verify idempotency: The header must have include guards or
#pragma once. - Update all TUs: Replace
#include "header.h"withimport "header.h";in every TU. Do not mix#includeandimportfor the same header across different TUs. - Update the build system: Ensure the build system is configured to scan for header unit imports and compile the BMI.
- Verify no macro dependencies: If any TU previously relied on macros defined by the header being visible after the include, those macros are now visible only via the
import(header unit semantics). This is fine but may break if macros were used in the includer’s code after the include point.
18. Header Units and import std; Coexistence
Section titled “18. Header Units and import std; Coexistence”In a project that uses both header unit imports and the import std; named module, the build system Must handle the interaction between the two. Key considerations:
- No double import: If a header unit
import <vector>andimport std;are both used in the same TU, the build system must ensure that the declarations from<vector>are not defined twice (once from the header unit BMI, once from thestdmodule BMI). Most compilers handle this deduplication internally, but some may report ambiguity errors. - Different macro visibility:
import std;provides macro hygiene (no macros visible), whileimport <cstdio>exports macros. Mixing them in the same TU creates asymmetric behavior that can confuse developers. - BMI ordering: The
stdmodule BMI must be built before any header unit BMI that depends on standard library headers. The build system must enforce this ordering.
Recommendation: Do not mix individual standard library header unit imports with import std; in The same project. Pick one approach and use it consistently.
19. Header Units and Tooling Support
Section titled “19. Header Units and Tooling Support”As of 2025, the tooling ecosystem for header units is still maturing:
| Tool | Header Unit Support | Notes |
|---|---|---|
| CMake | 3.28+ | P1689 scanning, BMI management |
| Clangd | 18+ | Semantic analysis of header unit imports |
| MSVC IntelliSense | VS 2022 17.10+ | Good support for .ifc header units |
| GCC | 14+ | Experimental, no P1689 support |
| Ninja | 1.11+ | dyndep support for module dependencies |
| Make | None | Cannot manage BMI dependencies |
For projects that rely heavily on IDE features (go-to-definition, auto-complete), Clangd 18+ with Clang 18+ provides the most reliable experience. MSVC’s IntelliSense is also reliable. GCC’s Experimental support means that header unit-based projects may encounter IDE issues.
See Also
Section titled “See Also”- Binary Module Interfaces (BMI)
- The C Runtime (CRT)
- C++20/23 Modules Overview
- Linker
- Preprocessing and the AST
Summary
Section titled “Summary”This topic covers the essential concepts and techniques related to header units, including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.