Modules
C++20 modules — export/import, interface vs implementation units, the Binary Module Interface, partitions, and migration from
17 questions
JuniorTheoryVery commonWhat does the export keyword do in a module interface?
What does the export keyword do in a module interface?
export marks a declaration as part of the module's public API, making it visible to anyone who writes import. Declarations without export stay internal to the module. export also forms the module declaration itself: export module name;.
Common mistakes
- ✗Confusing
export(visibility/API membership) with linkage — they are independent concepts - ✗Assuming all declarations in a module interface are exported by default
- ✗Thinking
exportchanges access control likepublic/privateinside a class
Follow-up questions
- →Can you
exportastaticfunction or something from an anonymous namespace? - →How does an
export { ... }block differ from prefixing each declaration?
JuniorTheoryVery commonHow does import differ from #include for the consumer of code?
How does import differ from #include for the consumer of code?
#include is preprocessor text substitution — the header's text is pasted in and re-parsed. import loads an already-compiled Binary Module Interface, so macros do not leak in, import order is irrelevant, and only exported names become visible.
Common mistakes
- ✗Expecting macros defined in a module to be usable after
import - ✗Believing
importis processed by the preprocessor like#include - ✗Assuming reordering
importlines can change compilation results the way#includeorder can
Follow-up questions
- →Why is
importallowed to be order-independent but#includeis not? - →Can you still use
#includeandimporttogether in the same file?
JuniorTheoryVery commonWhat problems with the #include model do C++20 modules address?
What problems with the #include model do C++20 modules address?
#include textually copies a header into every translation unit, so the same code is re-parsed repeatedly (slow builds), macros leak across files, include order matters, and there is no real encapsulation. Modules fix all four by compiling an interface once and isolating it semantically.
Common mistakes
- ✗Believing include guards solve the re-parsing cost — they only prevent double inclusion within one TU, not across TUs
- ✗Thinking macro leakage is a style issue rather than a structural defect of textual inclusion
- ✗Assuming the problems are purely about speed and ignoring encapsulation and order-dependence
Follow-up questions
- →Why don't include guards eliminate the repeated parsing cost across translation units?
- →Which of these problems can a precompiled header solve, and which can it not?
JuniorTheoryCommonWhat is a Binary Module Interface (BMI)?
What is a Binary Module Interface (BMI)?
A BMI is a compiler-generated binary file holding the already-parsed semantic representation of a module interface. Consumers read the BMI instead of the source .cppm. Extensions vary: .gcm (GCC), .pcm (Clang), .ifc (MSVC).
Common mistakes
- ✗Confusing the BMI with the object file — BMI holds parsed semantics, not machine code
- ✗Thinking a BMI is portable across compilers because the source language is standardized
- ✗Believing the BMI is human-readable text like preprocessor output
Follow-up questions
- →Should BMI files be committed to version control? Why or why not?
- →What still goes into a separate object file even when a BMI exists?
JuniorTheoryCommonWhat is the difference between a module interface unit and an implementation unit?
What is the difference between a module interface unit and an implementation unit?
An interface unit starts with export module name; and declares the module's public API — it is what consumers' import sees. An implementation unit starts with module name; (no export) and only defines bodies; consumers never see or compile it directly.
Common mistakes
- ✗Thinking consumers must compile or import implementation units to get definitions
- ✗Believing the interface unit cannot contain inline definitions
- ✗Confusing
module name;(implementation) withexport module name;(interface)
Follow-up questions
- →Can a single module have more than one primary interface unit?
- →Where does an implementation unit's code end up — in a BMI or an object file?
JuniorTheoryCommonWhat happens to declarations in a module that are not marked export?
What happens to declarations in a module that are not marked export?
They remain fully usable inside the module — across its interface and implementation units — but are invisible to any consumer that writes import. This is real encapsulation: helper functions and details stay private without needing a separate detail namespace convention.
Common mistakes
- ✗Thinking non-exported entities are dead-stripped and unusable within the module
- ✗Believing a fully qualified name lets consumers reach non-exported declarations
- ✗Assuming everything in the BMI is automatically importable
Follow-up questions
- →How does this replace the old
namespace detailconvention for hiding helpers? - →Can a non-exported type be used as the return type of an exported function?
MiddleTheoryCommonWhy does the BMI model compile faster than #include?
Why does the BMI model compile faster than #include?
With #include, every translation unit re-parses the header's full text. A module interface is parsed once into a BMI; every consumer then reads that pre-digested semantic representation instead of re-tokenizing and re-analyzing the same source again and again.
Common mistakes
- ✗Thinking the BMI stores machine code, so consumers skip code generation
- ✗Believing include guards already give the same caching benefit across TUs
- ✗Attributing the win to parallel builds rather than parse-once reuse
Follow-up questions
- →Why don't include guards give the same parse-once benefit across translation units?
- →Does the BMI need template definitions inside it, or just declarations?
MiddleTheoryCommonWhy can't a module export macros?
Why can't a module export macros?
Macros are a preprocessor concept with no scope, and stopping their uncontrolled leakage is a core goal of modules. import transfers compiled semantic entities, not preprocessor text, so a #define inside a module simply never reaches the consumer.
Common mistakes
- ✗Believing macro non-export is a temporary compiler limitation rather than by design
- ✗Thinking
export #defineis valid syntax - ✗Confusing macros (preprocessor) with constants —
constexprvalues can be exported
Follow-up questions
- →If your API depends on macros, how should you adapt it for modules?
- →Can a header unit's macros reach the consumer, unlike a named module's?
MiddleTheoryOccasionalHow does a BMI differ from a precompiled header?
How does a BMI differ from a precompiled header?
A PCH is a non-standard compiler cache of a header's textual state — macros still leak and one PCH usually serves the whole project. A BMI is a standard module artifact with real encapsulation: only exported names are visible and macros never cross the import boundary.
Common mistakes
- ✗Treating a BMI as just a standardized precompiled header with the same semantics
- ✗Thinking a PCH gives the same macro isolation that a module import does
- ✗Assuming a PCH can express per-module encapsulation of exported vs internal names
Follow-up questions
- →Why can a project realistically have many BMIs but usually only one PCH?
- →Does a PCH change the meaning of code, or only its build speed?
MiddleTheoryOccasionalWhy do modules require build-system support beyond the compiler?
Why do modules require build-system support beyond the compiler?
A module's BMI must be built before any consumer that imports it, so the build is now dependency-ordered. The build system must scan sources to discover import edges and schedule compilation in topological order — plain file timestamps are not enough.
Common mistakes
- ✗Thinking compilation order is still free and parallel like with headers
- ✗Believing dependency edges can be derived without scanning for
importstatements - ✗Assuming a classic Makefile handles modules with no changes
Follow-up questions
- →What does a module dependency scanner produce, and when does it run?
- →Which build systems gained native module support, and which still struggle?
MiddleTheoryOccasionalWhat does export import do in a module?
What does export import do in a module?
export import X; imports module or partition X and re-exports its names to anyone importing the current module. A plain import X; keeps X private; export import builds facade modules and joins partitions into a primary interface.
Common mistakes
- ✗Thinking
export importcreates a bidirectional dependency between modules - ✗Believing it also re-exports the imported module's non-exported declarations
- ✗Assuming plain
importalready makes the imported names visible to downstream consumers
Follow-up questions
- →How does
export import :partition;help assemble a primary module interface? - →If module A does
export import B, does a cycle B→A become legal?
MiddleTheoryOccasionalWhat is the global module fragment and what is it for?
What is the global module fragment and what is it for?
It is the region opened by a bare module; line, before export module name;, where legacy #include directives are allowed. Entities pulled in there are usable inside the module but are not exported to consumers — it is the compatibility bridge to old headers.
Common mistakes
- ✗Thinking entities from a global-fragment
#includeare re-exported to consumers - ✗Placing the
module;line afterexport module name;instead of before it - ✗Believing the global fragment is where the module's public API goes
Follow-up questions
- →Why must
#includego in the global fragment rather than the module body? - →How is this different from converting the header into a header unit?
MiddleTheoryOccasionalWhat is a header unit, and how does it differ from a named module?
What is a header unit, and how does it differ from a named module?
A header unit is an existing header compiled into BMI form and pulled in via import <vector>;. It removes re-parsing cost, but unlike a named module its macros still reach the consumer — so it is a migration bridge, not full module encapsulation.
Common mistakes
- ✗Assuming a header unit isolates macros the way a named module does
- ✗Thinking a header unit must be a newly authored file rather than an existing header
- ✗Believing
import "foo.h";is a plain alias for#includewith no BMI involved
Follow-up questions
- →Why do header-unit macros still leak while named-module macros do not?
- →When would you prefer a header unit over fully converting code to a named module?
MiddleTheoryOccasionalWhat is a module partition and when do you use one?
What is a module partition and when do you use one?
A partition is a named sub-unit of one module, written export module math:basic;. Partitions let you split a large module across files while still presenting one module to consumers. The primary interface gathers them, often via export import :basic;.
Common mistakes
- ✗Thinking consumers import a partition directly with
import math:basic; - ✗Confusing partitions (organization within one module) with separate modules
- ✗Believing partitions provide encapsulation rather than file-level structure
Follow-up questions
- →What does
export import :basic;in the primary interface unit accomplish? - →Can a partition contain only an implementation and not be exported at all?
SeniorDesignOccasionalYou inherit a large C++ codebase built entirely on #include headers and are asked to move it to C++20 modules. The team cannot stop feature work, so the migration must proceed in small steps that keep the project compiling at every commit, and some third-party headers cannot be converted at all. Describe how you sequence the migration, how converted and not-yet-converted code coexist during the transition, and what build-tooling prerequisite must be in place first.
You inherit a large C++ codebase built entirely on #include headers and are asked to move it to C++20 modules. The team cannot stop feature work, so the migration must proceed in small steps that keep the project compiling at every commit, and some third-party headers cannot be converted at all. Describe how you sequence the migration, how converted and not-yet-converted code coexist during the transition, and what build-tooling prerequisite must be in place first.
Migrate incrementally, bottom-up: start with leaf libraries that have no outgoing dependencies, wrap legacy headers in the global module fragment, and move up the dependency graph. Use header units for not-yet-converted deps; require a module-aware build system like CMake 3.28+.
Open full question →Common mistakes
- ✗Attempting a single big-bang conversion instead of an incremental, leaf-first one
- ✗Forgetting that
#includeandimportcan coexist during the transition - ✗Ignoring that the build system must be upgraded before any conversion works
Follow-up questions
- →Why is leaf-first ordering necessary rather than just convenient?
- →How do you keep a header and its module version in sync to avoid ODR issues?
SeniorDebuggingOccasionalWhat ODR hazard arises when mixing #include and import of the same code?
What ODR hazard arises when mixing #include and import of the same code?
If one TU #includes a header while another imports a module wrapping the same code, the same entity gets two definitions through different paths. They must be token-for-token identical; any drift between header and module is an ODR violation, often silent and unchecked.
Common mistakes
- ✗Believing the linker reliably detects and rejects clashing definitions from the two paths
- ✗Assuming the ODR violation is always a hard compile error rather than silent UB
- ✗Thinking
importand#includeof the same code are guaranteed to be equivalent
Follow-up questions
- →Why is an ODR violation often undiagnosed rather than a hard error?
- →What discipline keeps a header and its module wrapper from drifting apart?
SeniorTheoryRareWhy must each toolchain rebuild a module's BMI itself?
Why must each toolchain rebuild a module's BMI itself?
A BMI is a private serialization of one compiler's internal semantic representation; the C++20 standard never defines its format. It is unportable between compilers and often between versions of the same one, so every toolchain must regenerate it from source.
Common mistakes
- ✗Believing the C++ standard defines a portable BMI format
- ✗Thinking same-ABI compilers can therefore share BMI files
- ✗Assuming the BMI holds machine code, making it architecture- rather than compiler-bound
Follow-up questions
- →Why is even a minor compiler version bump often enough to invalidate a BMI?
- →What does BMI non-portability imply for distributing a precompiled library?