Chapter 14 of 24 | C++
Modules, imports, and declarations
A declaration introduces a name and tells the compiler how it can be used. A definition supplies the entity itself, such as a function body or object storage. Understanding the difference helps you organize a program into reusable files without accidentally defining the same entity multiple times.
Headers and the preprocessor
// geometry.hpp
#pragma once
struct Point {
double x{};
double y{};
};
double distance(Point first, Point second);
A source file includes a header with #include "geometry.hpp". The preprocessor textually includes the header before compilation. Header guards or the widely supported #pragma once prevent repeated inclusion within a translation unit. Keep headers self-contained: include what they directly need and avoid unrelated imports.
Put declarations in headers and non-template function definitions in source files when they should have one program-wide definition. Inline functions and templates are commonly defined in headers because their definitions must be visible where they are used. Avoid defining non-inline global objects in headers.
C++ modules
C++20 introduced named modules. A module interface can export declarations, and another source file can import that module. Modules aim to reduce textual inclusion and improve isolation, but compiler and build-system support is still toolchain-dependent. The learning examples in this course use headers as the portable baseline and introduce modules only after you verify your compiler's support.
// Example shape; build support varies by compiler.
export module geometry;
export struct Point {
double x{};
double y{};
};
A module is not simply a header with a different extension: it has its own interface and compilation model. Follow the selected toolchain's documented module workflow rather than assuming a single universal command.
Macros and includes
Macros perform token substitution before C++ parsing. They do not obey ordinary scope or type rules, so prefer constants, functions, templates, and language features when they can express the same intent. Use macros only when the preprocessor is genuinely required, and give them distinctive names.
Why headers need care
An included header is processed as part of every source file that includes it. If that header includes many unrelated headers, all of those dependencies are pulled into every translation unit, increasing build time and making the interface harder to understand. Include only what the declarations in that header require. An implementation file can include additional headers for its private work.
Header guards prevent repeated inclusion within one translation unit. They do not solve every One Definition Rule problem: the same header can be included once by several different source files. A plain function or global object definition in a header may therefore create multiple program-wide definitions. Use one source-file definition or an appropriate inline definition.
Forward declarations and incomplete types
Sometimes a header only needs to mention that a class exists, not its complete layout. A forward declaration such as class Renderer; can reduce dependencies in certain interfaces. A pointer or reference can refer to an incomplete class type, but code that constructs an object or accesses its members needs the full definition. Forward declarations are not appropriate for every type, and they must not conflict with the actual declaration.
This technique can improve compile times in larger codebases, but overusing it makes dependencies harder to follow. Include the defining header when the class layout or its members are needed, and keep public interfaces understandable to their users.
What modules change
Modules create named interfaces that consumers import rather than repeatedly textually including header contents. Exported declarations form the public interface; unexported declarations can remain implementation details. Module compilation may produce compiler-specific artifacts, so the build system must understand the compiler's module workflow and dependency ordering.
Modules do not make declarations unnecessary or guarantee that every implementation detail should be exported. A good module still needs a thoughtful interface, explicit dependencies, and reproducible builds. Because compiler support evolves, verify the exact feature set for the toolchain before using modules in a project that must build across several environments.
Macros can break ordinary expectations
A macro is expanded by token substitution before the compiler understands C++ types and scopes. A macro argument may be evaluated more than once if the replacement text uses it multiple times, and a macro name can unexpectedly replace another identifier. Parentheses help with some expression macros, but they do not turn macros into type-safe functions. Prefer constexpr values, inline functions, templates, or concepts when those features meet the need.
Practice
- Separate a small function declaration into a header and its definition into a source file.
- Explain why including a header twice should not create duplicate declarations.
- Check your compiler's current module status before attempting a module-based example.
- Explain why an include guard and the One Definition Rule solve different problems.
- Move a private implementation include out of a public header when it is no longer needed there.
- Compare a macro, a
constexprvalue, and an inline function for a small reusable operation.
Key idea: interfaces belong in declarations; definitions need a deliberate home. Headers remain a practical cross-toolchain foundation, while modules are a newer option with varying support.
Header dependencies and compile-time architecture
Textual inclusion makes a header's transitive dependencies part of every translation unit that includes it. A small public header can therefore affect build time and macro state across a large project. Include what a header needs to compile on its own, forward-declare only where the language permits an incomplete type, and keep implementation-only dependencies out of public interfaces.
Include order should not change a program's meaning. A header that relies on some unrelated header to define a type first is not self-contained. Automated header checks or a test translation unit that includes each public header first can expose this defect early.
Modules, partitions, and build artifacts
A named module has a module interface unit that can export declarations. A module implementation unit can provide definitions, and partitions can divide a large module's internal interface. Importing a module is not ordinary text substitution: the compiler consumes a compiled module interface, often represented by an implementation-specific binary artifact.
Because module artifacts depend on compiler, configuration, and dependency order, build systems must know how to produce them. Module support has advanced over time, but compiler versions may implement different subsets of the standard. A portable project should define its supported toolchains and test the actual module workflow on each one rather than infer support from a standard-mode flag alone.
Header units and interoperability
Header units can allow some headers to be imported as compiled units, but macro behavior and compatibility with textual inclusion require care. Modules and headers can coexist during migration. Do not assume that importing a header has identical effects to including it in every respect, particularly when code relies on macros or implementation-specific build behavior.
Declarations, templates, and inline definitions
Templates usually need their definitions visible where instantiation occurs, which is why template implementation is commonly placed in headers. Modules can export templates, but the module interface must still make the necessary definition available to the importer through the module mechanism. Inline functions and variables may have definitions in multiple translation units under ODR requirements; inline is primarily an ODR/linkage property, not merely a request that the compiler substitute the function body at a call site.
Constrained declarations can improve a template's interface. Keep the declaration, concept requirements, and implementation aligned so a caller can understand why a type is accepted or rejected without reading unrelated implementation details.
Advanced exercise
- Measure build time in a small project with a heavily included header and reduce its unnecessary dependencies.
- Build a module interface with your chosen compiler and document the exact toolchain and build-system requirements.
- Explain why marking a function
inlinedoes not guarantee machine-code inlining. - Compare a template definition in a header with a non-template definition in a source file.