C++ Private Module Fragments Explained
C++20 introduced modules to eliminate the physical and logical drawbacks of header files, including long build times and leaky macro scopes. While developers often split modules into separate interface and implementation files to prevent implementation edits from triggering downstream recompilations, C++ also provides an alternative: the private module fragment. This article explains how the private module fragment functions within a primary module interface unit to fully encapsulate internal implementation details, decouple internal code changes from consumers, and maintain the convenience of a single-file module architecture.
The Challenge of Single-File Module Interfaces
In a standard C++ module interface unit, any non-exported declaration
(such as internal helper functions, private variables, or non-exported
classes) still contributes to the generated Compiled Module Interface
(CMI or BMI). Even though downstream translation units cannot access
entities without the export keyword, the compiler includes
the interface unit's entire AST or symbolic representation in the
CMI.
Consequently, changing the body of a non-exported helper function or modifying an internal type inside an interface unit often changes the hash or signature of the resulting CMI. When the CMI changes, the build system assumes the interface has changed and forces every consumer that imports the module to recompile.
How the Private Module Fragment Works
The private module fragment is marked using the syntax
module :private;. It acts as an absolute boundary within a
primary module interface unit, dividing the file into two distinct
sections: the observable interface section and the unreachable
implementation section.
export module MathEngine;
// Interface Section: Exported and non-exported declarations
export int computeResult(int input);
// Private Module Fragment begins here
module :private;
// Implementation Section: Fully concealed from the interface
int internalHelper(int n) {
return n * n + 42;
}
int computeResult(int input) {
return internalHelper(input);
}When the compiler encounters module :private;, it alters
how it processes the subsequent code:
- Exclusion from the Module Interface: Everything
declared after
module :private;is considered part of an implementation translation unit rather than the interface. The compiler does not emit these declarations into the CMI used by downstream importers. - Recompilation Shielding: Because internal function
bodies and helper declarations after
module :private;do not affect the CMI, modifying them will not invalidate the interface file. Build systems notice that the interface signature remains identical, preventing cascading recompilation of dependent modules. - No External Reachability: Entities in the private
module fragment can never be found by name lookup outside of that
translation unit, nor can they be exported. The
exportkeyword is explicitly ill-formed inside a private module fragment.
Rules and Constraints
To maintain modular integrity, the C++ standard enforces strict rules on the private module fragment:
- Primary Interface Only: It can only be used in the primary module interface unit. It cannot appear in module implementation units or module partitions.
- Terminal Position: It must be the final declaration block in the file. No other declarations or partitions may follow it.
- No Inline Expansions: Exported functions defined inline in the interface cannot depend on types or entities declared inside the private fragment, as the consumer's compiler will not have visibility into the private section during call-site code generation.
Summary of Architectural Benefits
The private module fragment provides the isolation benefits of
traditional source files (.cpp) combined with the cohesion
of keeping an entire component within a single source file. By
segregating definitions behind module :private;, C++
developers achieve true data hiding, prevent symbol pollution, and
protect consumer translation units from unnecessary rebuild cycles when
internal implementation logic changes.