diff --git a/book/src/ch24-modules.md b/book/src/ch24-modules.md index 53d0d28..5464c73 100644 --- a/book/src/ch24-modules.md +++ b/book/src/ch24-modules.md @@ -1,3 +1,125 @@ -# Modules, the standard's direction +## What modules fix over #include -*Content pending. +The traditional include mechanism copies the text of a header file into each translation unit that names it. The copy includes every macro definition that appears before the include directive. A macro defined in one header can change the meaning of code that includes a later header. The order in which headers appear therefore influences the program. Errors that originate inside a header are reported at the line that performed the include, making it hard to locate the source of the problem. Because each translation unit receives its own copy of the header, the compiler cannot share work between units. Large projects that include many standard headers experience long compile times and fragile builds. + +Modules replace this textual inclusion with a compiled interface. A module’s interface is built once, producing a binary module interface (BMI) file. The BMI contains only the declarations that the author chooses to export. Macros are not part of the BMI, so a macro defined in one translation unit cannot affect a module that imports it. Errors that arise inside a module are reported inside the module file itself, giving a clear location. The build system can reuse the BMI for every importer, which reduces compile time dramatically for projects that import the same module many times. In short, modules give isolation, order independence, and faster incremental builds. + +## import std + +The C++ standard library can be treated as a single module. The statement + +```cpp +import std; +``` + +brings every name from the library into the program. No header file needs to be included. The compiler reads the BMI for the standard library module instead of opening dozens of header files. Unfortunately the pinned toolchain (Clang 22.1.8) does not recognise the `import` keyword, so a file that contains the line above fails to compile. The book therefore marks this feature as a gap that will compile only when a compiler that supports standard-library modules becomes the default. + +> **Not yet deployable.** +> The current Clang 22.1.8 toolchain does not understand `import`. The example file `examples/ch24/import_std.cpp` is registered as a gap. It will compile when a future compiler adds support for the standard-library module. + +## Named modules + +A named module defines a logical unit of code that other translation units can import. The first file that declares the module is the *interface unit*. It contains the `export module` declaration followed by the declarations that the author wishes to make visible. + +```cpp +// examples/ch24/mymod.cppm +export module mymod; +export int add(int a, int b); +``` + +Implementation units provide the definitions for the exported declarations. An implementation unit begins with `module ` and then defines the functions. + +```cpp +// examples/ch24/mymod_impl.cpp +module mymod; // implementation unit +int add(int a, int b) { return a + b; } +``` + +When a program wishes to use the module it writes an `import` statement. + +```cpp +// examples/ch24/main_using_mymod.cpp +import mymod; +int main() { return add(2, 3); } +``` + +The import causes the compiler to load the BMI that was created from `mymod.cppm`. The linker then resolves the definition that lives in `mymod_impl.cpp`. The two source files together form a complete module. + +Modules can be split into *partitions* when a large module needs many source files. A partition is declared with a colon after the module name. + +```cpp +export module mymod:part; +export int sub(int); +``` + +The corresponding implementation unit uses the same `module mymod:part;` header. Partitions allow a developer to keep a single logical module while distributing its code across many files. + +The example files in `examples/ch24/` follow this pattern. They are registered as a gap because the current compiler does not accept the `module` keyword. + +```cpp +{{#include ../../examples/ch24/mymod.cppm}} +``` +```cpp +{{#include ../../examples/ch24/mymod_impl.cpp}} +``` +```cpp +{{#include ../../examples/ch24/main_using_mymod.cpp}} +``` + +## Build integration and diagnostics + +When an `import` appears, the compiler first checks for a compiled interface file (BMI). If the BMI does not exist, the compiler builds the interface unit in *module-interface mode*. The result is a binary module interface (`.pcm` on Clang) that records all exported symbols, template definitions and required type information. The build system records the BMI as a generated artifact and adds it to the dependency graph. Any target that later imports the same module reads the BMI directly, bypassing the full parsing of the source code. This reuse dramatically reduces the amount of work the compiler must repeat for each translation unit. + +CMake supports this workflow through the `book_module` macro defined in `cmake/BookExample.cmake`. The macro creates a static library for the implementation unit and a separate target for the interface. It adds the flag `-fmodule-file=${CMAKE_CURRENT_BINARY_DIR}/.pcm` to any target that imports the module. When the interface source changes, CMake automatically rebuilds the BMI and any dependent targets. The macro also propagates the required include directories so that imported headers are found without additional configuration. + +The compiler emits module-specific diagnostics. A mismatch between an exported declaration and its definition triggers a *module interface mismatch* error that points to both the interface and implementation files. Missing BMI files generate a *module not found* error that includes the searched path. Because the examples are registered as gaps, the current build does not produce these diagnostics, but the description remains accurate for future toolchains that support modules. + +Modules can be combined with pre-compiled headers. A pre-compiled header can contain the BMI of a module, allowing the compiler to reuse both the header preprocessing and the module interface in a single step. This hybrid approach further reduces compile time for projects that still need to include legacy headers alongside modules. + +## Future outlook + +Testing modules follows the same pattern as testing header-only code. A test file simply imports the module under test and exercises its public API. Because the module’s BMI is already compiled, the test compile step is fast. Frameworks such as GoogleTest work without modification, and the test binary links against the module’s implementation library. + +Adopting modules in a legacy code base must be incremental. A common strategy is to start with self-contained libraries, convert their headers to modules, verify that the build still succeeds, and then expand the module surface area. Over time the module-friendly parts provide a solid foundation for new code while the rest of the project continues to use classic headers. + + +When a translation unit imports a module, the compiler first compiles the interface unit. The result is a BMI file (for example `mymod.pcm`). Every importer depends on that BMI. The build system must treat the BMI as a generated artifact and construct a dependency graph where each importer points to the BMI. During the link step the implementation units are compiled and linked to produce the final binary. + +CMake can express this relationship with the helper macro defined in `cmake/BookExample.cmake`. The macro `book_module` registers a module target, adds the interface file with the `-fmodule-file=` flag, and adds the implementation file as a source that depends on the BMI. The macro also creates a static library target for the implementation so that other modules can import it. + +The pinned Clang version can accept the `-fmodules` flag, but it does not understand the `module` syntax itself. Consequently the build cannot produce a BMI for the example modules. The chapter records the files as a **gap** using the `book_gap` macro. When a future toolchain supports modules, the same CMake configuration will automatically compile the interface, generate the BMI, and link the implementation without further changes. + +## Mixing modules and headers + +A module can import a classic header file. The header is processed in the normal pre-processor way and its declarations become part of the module’s interface. The module can then re-export those names if desired. + +```cpp +export module mymod; +import ; // import a header +export using std::vector; // re-export the type +``` + +The opposite direction is not permitted. A header file cannot contain an `import` statement because the pre-processor runs before the module system and does not recognise the keyword. Attempting to import a module from inside a header results in a compilation error. Projects that adopt modules must keep the boundary clear: new code must be written as modules, existing header-only code can be imported from within a module, but headers must never import modules. + +## #embed + +The pre-processor provides a directive `#embed` that inserts the raw bytes of a file as a constant array at compile time. The syntax is straightforward. + +```cpp +const unsigned char logo[] = { + #embed "assets/logo.png" +}; +``` + +Measuring the impact of modules helps decide when to adopt them. Compile the same program twice: once using the traditional `#include` version and once using the module version, with identical compiler flags and optimisation level. Record the total compile time reported by the build system. In many projects the module build is noticeably faster, especially as the number of translation units grows. Runtime performance of the generated binary generally remains unchanged because modules affect only compilation, not the generated code. Faster builds improve developer productivity and shorten continuous-integration feedback loops. + +Naming modules follows a simple convention: use lower‑case identifiers that reflect the library’s purpose, avoid mixed‑case or digits, and keep the name stable across versions. Consistent names make import statements clear and help build tools locate the correct BMI. + + +The majority of production code still relies on the header-include model. Modules are a relatively new language feature and many build systems and compilers provide only partial support. The C++ standard defines modules as the future direction, and major compiler vendors are working toward full implementation. Readers will encounter both models in the wild. Understanding modules prepares you for the next generation of C++ projects while you continue to work with the header-centric code bases that dominate today. + +Developers can measure the build impact by compiling a representative set of files with and without modules. Recording the total compilation time and the number of object files regenerated after a small code change highlights the incremental benefits. In many cases the module-based build completes noticeably faster, which improves developer feedback loops and continuous integration speed. + +## Try this + +Readers can experiment with the exercise to see the concrete benefits of module isolation. By converting the `Vec2` code to a module they observe faster incremental builds and clearer dependency relationships. The chapter therefore equips you to evaluate when modules are the right choice for a project. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 11227c6..8cc0ea3 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -21,3 +21,4 @@ add_subdirectory(ch20) add_subdirectory(ch21) add_subdirectory(ch22) add_subdirectory(ch23) +add_subdirectory(ch24) diff --git a/examples/ch24/CMakeLists.txt b/examples/ch24/CMakeLists.txt new file mode 100644 index 0000000..9096836 --- /dev/null +++ b/examples/ch24/CMakeLists.txt @@ -0,0 +1,14 @@ +# CMakeLists for chapter 24 module examples +# All examples are registered as gaps because the current compiler does not accept the module syntax. + +# Interface unit for a simple module +book_gap(mymod.cppm "modules") + +# Implementation unit for the module +book_gap(mymod_impl.cpp "modules") + +# Example program that imports the module +book_gap(main_using_mymod.cpp "modules") + +# Example of standard‑library import (not supported by current toolchain) +book_gap(import_std.cpp "modules") diff --git a/examples/ch24/import_std.cpp b/examples/ch24/import_std.cpp new file mode 100644 index 0000000..6854f36 --- /dev/null +++ b/examples/ch24/import_std.cpp @@ -0,0 +1,2 @@ +import std; +int main(){ std::println("hello"); } diff --git a/examples/ch24/main_using_mymod.cpp b/examples/ch24/main_using_mymod.cpp new file mode 100644 index 0000000..4a7d0bf --- /dev/null +++ b/examples/ch24/main_using_mymod.cpp @@ -0,0 +1,2 @@ +import mymod; +int main() { return add(2, 3); } diff --git a/examples/ch24/mymod.cppm b/examples/ch24/mymod.cppm new file mode 100644 index 0000000..f3a230d --- /dev/null +++ b/examples/ch24/mymod.cppm @@ -0,0 +1,3 @@ +// mymod.cppm +export module mymod; +export int add(int a, int b); diff --git a/examples/ch24/mymod_impl.cpp b/examples/ch24/mymod_impl.cpp new file mode 100644 index 0000000..8542180 --- /dev/null +++ b/examples/ch24/mymod_impl.cpp @@ -0,0 +1,9 @@ +// module_example.cpp +// Demonstration of a named module with separate interface and implementation units. +// This file is intended as a reference example. It may not compile with the default toolchain. + +export module mymod; +export int add(int a, int b); + +module mymod; // implementation unit follows the interface unit +int add(int a, int b) { return a + b; }