A Pragmatic Journey into C++ Modules
A Pragmatic Journey into C++ Modules
Rudyard Merriam
Buy on Leanpub

Table of Contents

Preface

Welcome to A Pragmatic Journey into C++ Modules. I am a retired software developer who keeps my brain active by studying C++, especially the improvements made with modern C++, specifically C++ 20 and C++23. However, I still find interesting bits all the way back to C++11. I’m also keeping an eye on C++26. I enjoy testing the improvements and writing about them from the perspective of a hands-on developer. As a result, my writing doesn’t always cover all the nuanced whys of the material, just the pragmatic, it works.

Why Modules?

Modules are the most impactful new addition to C++20 since they affect every developer. The other Big Four—concepts, ranges, and coroutines affect major segments of C++ users but not necessarily everyone.

I explored ranges and presented on them twice at CppNorth, once in 2023 and again in 2024. I touched on concepts since the two are intertwined. I highly recommend CppNorth if you are in the Western Hemisphere and unable to travel beyond it. The group is friendly and casual. CppCon is also great but a little more intimidating because all the names are there.

Who This Book Is For

This book is for developers who want to use modules but don’t know how to do so. It is especially for those who tried to use them but were frustrated determining how to build them properly. It was frustrating, and it took me a lot of searching to refine the material on the command line and CMake builds.

Omnibus books on C++20 with sections on modules and conference talks about them are available. However, they are limited in scope, and frankly, some of the earlier material is outdated as compilers and build systems, like CMake, have decided how to implement modules. The work isn’t complete across all support systems but clarified sufficiently to begin working earnestly with modules. These works didn’t delve into the day-to-day details of working with modules, and the examples were limited in scope and size.

This book aims to provide a pragmatic discussion, working from the simplest to the most complex example I can manage. Along the way, it will touch on the detailed workings of modules and build systems, particularly for GCC, Clang, and CMake. I run Linux, so I can’t discuss MSVC specifically but most should apply. By the end, you will have a comprehensive understanding of C++20 modules to apply to your projects.

The material in this book is built and runs using GCC 14.2, Clang 18.1.8, 19.1.7 and 20.1, and CMake 28.5 and 31.0 — or later versions if I can get them before publication. I work in CLion, but all will be tested with command-line compilation and CMake. All the code, CMake files, and command lines are included. There is a lot of code in this book.

Use of AI

I used AI tools while writing the book, but all the text is mine. ChatGPT provided information and context on modules that were verified from other sources. I also used Grammarly as a copyeditor.

Both tools were helpful but the accuracy is inconsistent. ChatGPT sometimes cites out-of-date or wrong information about modules. I spent hours a few times chasing suggestions about using CMake with modules.

Grammarly often changes the meaning in offered suggestions but I rely heavily on it for copyediting.

Conventions Used in This Book

The typographic conventions used are:

Italic introduces new term.

Bold marks something notable.

Constant Width follows the standard convention for code in text or program listing.

An icon indicating this blurb contains information

A section marked like this provides information both relevant and not.

An icon indicating this blurb contains a warning

A section marked like this is a caution to the unwary.

Naming Examples by Cat Breed

Each example uses the name of a breed of cats. This is more interesting than the blah chapter one, chapter two, etc and also provides an alphabetical ordering for chapters and examples.

Another reason for my unique naming system is my preference for inclusive language. I wanted names that didn’t carry the weight of the often-used Foo and Bar, which have their obscene origins in military usage dating back to WWII. Although I’m 76, I appreciate the societal shift towards inclusivity and feel that these terms oppose that goal. For this same reason, a talk on ranges used ‘meow,’ inspiring it as my go-to for naming.

Six (on right) and Seven of Nine (Not Jeri Ryan)
Figure 1.1. Six (on right) and Seven of Nine (Not Jeri Ryan)

Another motivation is our cats, Six and Seven of Nine, which we brought home five years ago as kittens. We initially referred to them by the last digits of the shelter’s serial number, a temporary solution that stuck longer than expected. When the time for a vet visit arrived, I suggested names from Star Trek, a show my wife was unfamiliar with. The vet took it all in stride since her cats were named after Marvel characters.

Notes to Early Readers

This is an early work-in-progress version of A Pragmatic Journey into C++ Modules. I’m publishing on LeanPub to release early and often. One reason is to gain feedback from early readers on unclear material or technical mistakes that need correction.

As an early reader, you obtain the book at a discount and receive updates and the final book at no additional charge. That’s your reward for being an early adopter. I thank you for helping me and for your patience during the process.

I hope to have the book finished by the end of May 2025.

When you comment, please mention the version you are using — PDF or EPub, the page and/or section, and the date of the release.

You can send comments to pragmatic@mysticlakesoftware.com.

2025-03-11, 12:07 a.m
First release.
  • The material up through Chapter 5 is complete, so I’m most interested in reviews for them.
2025-03-16
Second release.
  • Changed section titles to conform across chapters.
  • Added section on partitions and code without explanation. Partitions don’t want to build with CMake but are okay with GCC and Clang command lines. There is another version of Clang command lines that doesn’t want to work. Spent way too much time fighting with these problems.
  • Deep dive chapter incomplete but some substantive material there.
  • Marked chapters and section with (WIP) to indicate they are incomplete.
  • Comparison of Header and Module Builds (WIP) added.
  • Hid some raw chapters
2025-03-22
Work toward third release
  • Deep dive into modules chapter.
  • Renamed projects
  • Added Sphynx project
2025-03-24
Third release
  • Editing throughout to clarify
  • Dropped discussion of CMake and clang-tools on command line since Bill Hoffman advised they weren’t necessary and it now works. Perhaps a reboot or install of Clang 20 fixed the problem.

Into the Future with Modules

Modules replace the traditional header-based inclusion system inherited from C. Other modern or modernized languages support modules, demonstrating their relevance and importance. Even FORTRAN, the oldest language from 1957 and my first programming language, has adopted modules.

An icon indicating this blurb contains information

Another heralded feature added in C++20 is the three-way-comparison or spaceship operator(<=>). FORTRAN, as it was known way back then, had a three-way conditional, the arithmetic if:

1 IF (expression) label1, label2, label3

The label jumped to was selected by <0, ==0, or >0; maybe C++ will someday catch up fully with FORTRAN, although Fortran 90 abandoned this conditional. Will C++ follow again in a few decades?

C++ is now catching up by introducing modules in C++20, a significant update that brings the language closer to modern programming practices.

An icon indicating this blurb contains a warning

Exercise caution about considering a module as a quirky version of a header. This analogy is as misleading as viewing a reference as a peculiar form of a pointer. We’ve all encountered individuals who have fallen into this trap, leading to significant confusion. Modules and references, while addressing similar needs, do so in distinct, and hopefully more effective, ways.

Working With Larger Programs

Early programs were written using punched cards and tapes, some quite large or long. As a computer operator, I fed many 2500 card trays into the reader, although many cards may have been data. Nicholas Wirth’s original Pascal compiler was a one-pass compiler in a large deck of cards.

As computer systems developed, the programs became more extensive, and using multiple decks was unwieldy. Dropping the deck or experiencing a card jam that destroyed cards was devasatting. Storing programs on tapes or disks significantly improved code management.

Monolithic programs were challenging to work with. Developers broke code into mainline, functions, procedures, and subroutines to create manageable and reusable portions. It was natural to split the humongous mess into more distinct blocks for more effective use, but doing this was a problem.

Inclusion and Separate Compilation

An early approach broke the program source code into logical divisions, including a section where needed by other code. Many languages adopted this technique, including C with headers as an inclusion mechanism.

Language Era Separate Compilation? Inclusion Mechanism
C 1972–present (.c files are separate units) #include for headers
FORTRAN 1957–present (Fortran 90+ MODULEs) (Pre-90 COMMON blocks) INCLUDE for old-style inclusion
Pascal 1970–1990s (UNITs enable modular compilation) USES clause for inclusion
Ada 1980–present (PACKAGEs have separate compilation) None needed
COBOL 1959–present (mostly monolithic) COPY for inclusion
PL/I 1964–1980s (mostly monolithic) INCLUDE for inclusion
ALGOL 1958–1980s (typically whole-program compilation) Some implementations supported separate compilation

Separate compilation occurred in a similar timeframe, with some implementations of Algol 60 using it. It wasn’t until the mid-60s that it appeared in many other languages. C used it from its origin in the early ’70s.

Encapsulation

The next problem was that programmers would basically work around restrictions. (Who would have guessed?) Instead of using functions provided to access data, they worked directly with the data, often leading to inconsistencies that introduced bugs.

Encapsulation to hide details was introduced to minimize this problem. Modularity was used to implement encapsulation. Modula-2 (1978) was the first module system, with Ada following in a few years. The C language never adopted a module system.

As the object-oriented development paradigm swept in, bringing along C++, encapsulation strengthened since a class can prevent external changes.

Comparing Modules and Headers

Headers are an incomplete solution, and even C++ classes are insufficient. Implementation details leak into headers, and large headers used in multiple translation units are processed for each translation unit, increasing compilation time with no real gain.

Header organization can be a challenge when circular dependencies occur. Guards in headers are required to prevent multiple inclusions, which lengthen build times and cause possible inconsistencies. The information in the header becomes part of the global scope/namespace, which can lead to conflicts across libraries from different sources.

As this table shows, modules are intended to address the flaws of headers. Most expect this to occur, and experience in other languages supports this expectation.

Feature Header Files Modules
Compilation Speed Slow, due to repeated parsing Fast, as modules are compiled once
Encapsulation Weak, everything is exposed Strong, only exported symbols are visible
Dependency Management Hard, prone to cyclic issues Easy, avoids unnecessary dependencies
Global Namespace Pollution Yes, everything leaks No, only explicitly exported items
Security Risky, macros and internals leak Safe, controlled visibility

Module Basics

Modules are mechanisms for including code from other files while encapsulating the implementation details. Like static libraries, modules are compiled separately and linked into the application. This differs from headers, which are included directly into the source code, akin to a large cut-and-paste operation.

The impact on compilation time can be significant, as modules are compiled only once, while headers are compiled with each translation unit that includes them. Large header-based libraries, such as <fmt>, <regex>, or <print>, can significantly increase compilation time even for simple builds. Using modules can lead to a dramatic improvement in compile times.

Module Units

A module has multiple components: at least one primary Module Interface Unit (MIU) and zero or more Module Implementation Units. All of these are C++ translation units, i.e., source files.

Module Interface Unit

A Module Interface Unit provides the module’s user-facing interface. It exports the declarations for the variables, functions, or classes available from the module. It may also provide implementation definitions, but this is not recommended except for trivial modules.

There are two types of MIUs:

  • Primary Module Interface Unit
  • Module Partition Interface Unit

There can only be one primary MIU for a module, but there may be multiple Module Partition Interface Units. Each MIU generates a Built (or Binary) Module Interface (BMI), which contains information about the module or the partition. The BMI is used to quickly provide the declaration information to the translation units using the module. The BMI isolates the code using the module from the module’s implementation by delivering only the declarations. Application code only needs recompilation when the MIU, specifically the BMI, changes.

Module Implementation Unit

As the name implies, a Module Implementation Unit contains the source code defining the interface declared in the MIU. It may also provide code for other implementation units in the module. As with MIUs, there are two types of implementation units, which will be discussed in detail later.

  • Module Implementation Unit
  • Internal Module Partition Unit

These, along with their MIU counterparts, encapsulate the module’s internals.

A Module Interface Unit (Abyssinian)

This module is as tiny as you can get. It has a Module Interface Unit (MIU) that combines the interface and implementation in one file. The other translation unit is main.cpp. It illustrates the usage of some module-specific statements and demonstrates a module’s distinct sections.

Module Interface Unit: Abyssinian.cppm

Below is the Abyssinian module’s MIU example for this chapter. It has a .cppm extension to distinguish it from regular source code, which is a requirement by Clang but not by GCC. Microsoft uses its own specific extensions.

An icon indicating this blurb contains information

Clang also accepts the extensions .ccm, .cxxm, or .c++m. It can also process any extension when using the -x c++-module option.

Figure 3.1. Abyssinian.cppm
 1 module;
 2 
 3 #include <print>
 4 
 5 export module Abyssinian;
 6 
 7 using std::println, std::print;
 8 
 9 export auto miyāwi() -> void {
10    println("Ye'abisinya dmmmet miyāwi yilal:"); // Amharic transcription
11 }

Module statement

The first line is the module; (line 1) statement to indicate this is a module. Module Interface Unit (MIU) and Module Implementation Units are all C++ translation units. The keyword module distinguishes them from non-module code. The module; or similar statement must be the first code token in the file. Only comments may appear before the module statement.

The next module-specific statement is the export module statement (line 5), which declares the name of the module: Abyssinian.

The next export (line 9) places the function ‘miyāwi()’ into the module scope so it may be visible in other scopes. Without the export, miyāwi would be limited to module scope.

Global Module Fragment (GMF)

Between the module; statement (line 1) and export module declaration (line 5) is the global module fragment (lines 2-4). Code in the GMF is not part of the module and follows standard C++ scoping rules.

Officially, only preprocessing directives may appear in the global module fragment. This position roughly means only statements starting with a #, per cppreference. Note that anything in a header file may be included here.

Unfortunately, MSVC, GCC, and Clang all compile the following lines in the global fragment without error, although MSVC and GCC emit a warning.

Figure 3.2. Statements that Shouldn't Appear in the Global Fragment
 1 int x{};
 2 
 3 auto func() -> int {
 4    return x;
 5 }
 6 
 7 struct F {
 8    auto operator()() const -> int{return func();}
 9 };
10 using std::println, std::print;

All of these statements would be fine if placed in a header for inclusion.

Module Purview

The export module (line 5) declares the module and begins its definition. This statement begins the Module Purview area. Nothing in this area is global unless qualified with export; otherwise, everything is private to the module.

Module-specific statements, import and export, may be used here.

User Code: main.cpp

The code using the module is straightforward.

Figure 3.3. main.cpp
1 import Abyssinian;
2 
3 auto main() -> int {
4    miyāwi();
5 
6    return 0;
7 }

The main.cpp is simple. It imports the Abyssinian module (line 1), which brings the function miyāwi() into the scope and calls it (line 4). Importing the module makes miyāwi() available without qualification since modules do not introduce a separate namespace.

A Module Implementation Unit (Aegean)

This module is still tiny, with only two files in it; one is a Module Interface Unit (MIU) and the other a Module Implementation Unit.

Module Interface Unit: Aegean.cppm

The MIU is Aegean.cppm.

Figure 4.1. Aegean.cppm
1 module;
2 
3 export module Aegean;
4 
5 export auto νιάου() -> void;

There are three changes from the last chapter:

  • The definition of miyāwi() is removed,
  • The declaration of νιάου() is added (line 5),
  • The global fragment is empty since no headers are required.

With an empty fragment the module; statement may be omitted and the export (line 3) statement sufficient. I would include it as a convention. It may avoid a recompilation or two when you add a preprocessor statement after an import or export module statement.

Module Implementation Unit: aegean.cpp

The next file is a Module Implementation Unit.

Figure 4.2. aegean.cpp
1 module;
2 
3 #include <print>
4 
5 module Aegean;
6 
7 auto νιάου() -> void  {
8    std::println("Η Αιγαιόγατα λέει νιάου");
9 }

The module; (line 1) statement indicates it is part of a module. It is required here to start the global fragment to include the <print> header (line 3).

The implementation declares it is part of a specific module with module Aegean (line 5) statement. Finally, the function νιάου()(line 7) is defined.

User Code: main.cpp

The main.cpp is the same as for Abyssimian in the last chapter except for changing the module name and function name.

Figure 4.3. main.cpp
1 import Aegean;
2 
3 auto main() -> int {
4    νιάου();
5    return 0;
6 }

Building Aegean

This chapter demonstrates building Aegean. It uses command lines and CMake for GCC and Clang. The details will aid in adapting other build systems.

Building a program with modules requires creating the usual object files plus the Built Module Interface (BMI) .

Built Module Interface

As the name implies, the Built Module Interface is a precompiled and optimized binary version of the Module Interface Unit (MIU). It contains:

  • a representation of the exported declarations,
  • the dependencies among the imported modules,
  • type information for the exported classes, struts, and functions,
  • compiler version. (A Clang 18 BMI won’t work as a Clang 19 or GCC BMI.)

The BMI provides information for compiling code that uses a module but is not linked with the application. A BMI must be built before any code that uses it, including other members of the module unit.

Building with Command Lines

These commands build Aegean the previous chapter using GCC and Clang. Unfortunately, the two compilers use different compiler options to control building modules, making the situation a little bit difficult.

The tables in the next section show for GCC and Clang the new and existing command line options required to build modules.

Building With GCC

The GCC build begins by creating the BMI from the MIU. The output is a **GCC Compiled Module (GCM) ** and the standard .o object file. The generated BMI has the extension .gcm by default and resides in the gcm.cache subdirectory.

Option .cppm (BMI) .cpp using module
-fmodules-ts building a module building a module
-x c++ build BMI not used
-c do not link flags not to link
-r do not create executable do not create executable

The .cppm column is for builds of MIUs to generate a BMI and its object file. The .cpp column applies to main.cpp, application files using modules, and module implementation files.

GCC Command Lines

Here’s how to do the GCC build.

Figure 5.1. GCC Module Command lines
 1 # build the module unit interface - produces a .gcm in gcm.cache
 2 g++ -std=c++23 -fmodules-ts -c -x c++ Aegean.cppm -o bin/Aegean.o
 3 
 4 # build the module unit implementation and link with the MUI
 5 g++ -std=c++23 -fmodules-ts -c aegean.cpp -o bin/aegean.o -c
 6 g++ -std=c++23 -fmodules-ts -r bin/Aegean.o bin/aegean.o -o bin/Aegean.All.o
 7 
 8 # build main and link with the module
 9 g++ -std=c++23 -fmodules-ts -c main.cpp -o bin/main.o
10 g++ -std=c++23 -fmodules-ts  bin/main.o bin/Aegean.All.o -o bin/main

The first step (line 1) creates the BMI (.gcm) in the gcm_cache directory and the binary output in the bin directory using the .cppm file. The implementation file is built (line 5), and the two object files are linked (line 6) into a single object file (Aegean.All), making the module self-contained.

The final steps are to build the main() (line 9) and link it (line 10) to the module binary, which creates the executable.

After everything is complete, the output files are in these directories.

Figure 5.2. G++ Build Artifacts
1 ├── bin
2 │   ├── Aegean.o
3 │   ├── aegean.o
4 │   ├── main
5 │   └── main.o
6 ├──gcm.cache
7    └── Aegean.gcm

Building With Clang

The Clang build also begins by creating the BMI from the MUI. The output is a .pcm file, a PreCompiled Module. It then needs to build an object file. The location of the BMI is specified during the build. The .cppm extension is the norm expected by Clang but may be overriden with additional options.

Option .cppm (BMI) .cpp using module
-precompile build the BMI not used
-c do not link do not link
-r do not create executable do not create executable
-fmodule-file=<name>=bin/<name>.pcm* location and file for module

The .cppm column applies to builds of MIUs to generate the BMI and its object file (lines 2 and 3).

The .cpp column applies to the main.cpp (lines 10 and 11), application files using modules, and module implementation files (lines 6 and 7).

Clang Command Lines

Figure 5.3. Clang++ Module Command lines
 1 # build the module unit interface - produces a .pcm in bin
 2 clang++ -std=c++23 -fmodules --precompile Aegean.cppm -o bin/Aegean.pcm
 3 clang++ -std=c++23 -c Aegean.cppm -o bin/Aegean.o
 4 
 5 # build the module unit implementation and link with the MIU
 6 clang++ -std=c++23 -c -fmodule-file=Aegean=bin/Aegean.pcm aegean.cpp -o bin/aegean.o
 7 clang++ -std=c++23  -r bin/Aegean.o bin/aegean.o -o bin/Aegean.All.o
 8 
 9 # build main and link with the module
10 clang++ -std=c++23 -c -fmodule-file=Aegean=bin/Aegean.pcm main.cpp -o bin/main.o
11 clang++ -std=c++23  bin/Aegean.All.o bin/main.o -o bin/main

The BMI is built from the .cppm file (line 2) to create a .pcm.

In Clang’s terms, this is a two-phase build process that supports parallel compilation. The BMI and the object file are built in separate steps. The BMI is available for translation units that depend on it, without needing to wait for the object file. A one-phase version of the build process is demonstrated later.

Next, the MIU object file is built (line 3). The implementation file is built (line 7) and linked (line 9) with the MIU object file. Finally, main is built (line 13) and everything is linked (line 15).

The build output is placed in the output directories specified, in this case, bin, and the BMI, again bin.

The artifacts created by the build are the object files, the pcm, and the executable, main. Clang doesn’t use a specific cache directory like GCC so all files are in the bin directory.

Figure 5.4. Clang++ Build Artifacts
1  bin
2     ├── Aegean.o
3     ├── Aegean.pcm
4     ├── aegean.o
5     ├── main
6     └── main.o

Command Line Summary

I haven’t worked with command-line builds for years, possibly decades, so making these work was an adventure. After achieving success with these builds, I didn’t stop my research, continuing to read and watch talks. This continued study led to command-line improvements that you’ll see in later chapters. This approach is part of my “it works” attitude in the book. Once something works, you can improve it as you learn more. I sure do.

Building with CMake

Fortunately, CMake is the same for both compilers and works both within CLion and from the command line. At the bottom of each subdirectory’s CMake file, there is a command line for building the project with CMake.

Scanning for Modules

One reason modules took a few years to be fully available with CMake is the need to scan files to see if they are part of or use a module unit, which requires coordination between compilers and CMake. Fortunately, CMake had experience with Fortran, which implemented modules a number of years ago.

Clang uses the clang-tools to scan files and build a database, while GCC does this with the g++ compiler.

Parent Directory CMakelist.txt

The parent CMakelist.txt is typical except for adding the Clang line (line 4) at the beginning. Modules require specifying C++20 or later and activating the CMAKE_CXX_SCAN_FOR_MODULES (line 12) command to invoke the module scan. The other CMAKE_CXX statements assure that the correct standard is used and no extensions are included.

The real work is done in the subdirectory files.

Figure 5.5. Parent Directory CMakelists.txt
 1 cmake_minimum_required(VERSION 3.28.3)
 2 
 3 # must appear before project()
 4 set(CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS "/usr/bin/clang-scan-deps-18")
 5 
 6 project(pragmatic_modules LANGUAGES CXX)
 7 
 8 set(CMAKE_CXX_STANDARD 23)
 9 set(CMAKE_CXX_STANDARD_REQUIRED ON)
10 set(CMAKE_CXX_SCAN_FOR_MODULES ON)
11 
12 add_subdirectory(Aegean)
13 #... more subdirectories

Subdirectory CMakelist.txt

Here is the subdirectory CMakelist.txt.

Figure 5.6. Aegean Subdirectory CMakelists.txt
 1 cmake_minimum_required(VERSION 3.30.5)
 2 project(Aegean LANGUAGES CXX)
 3 set(module_name aegean)
 4 
 5 add_library(${module_name})
 6 target_compile_features(${module_name} PUBLIC cxx_std_23)
 7 target_sources(${module_name}
 8                PUBLIC
 9                FILE_SET cxx_modules TYPE CXX_MODULES
10                FILES ${PROJECT_NAME}.cppm
11                PRIVATE ${module_name}.cpp
12 )
13 
14 add_executable(${PROJECT_NAME} main.cpp)
15 target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
16 target_link_libraries(${PROJECT_NAME} ${module_name})
17 
18 # command line build
19 # rm -rf build && cmake -B build -G Ninja  && cmake --build build --verbose && build/Aegean

The project() statement (line 1) as always names the project. I add set(module_name aegean) (line 2) as a convenience to use ${module_name}, along with ${PROJECT_NAME} to easily change the names needed later in the file.

Kitware, the creators of CMake, reused add_library() (line 9) instead of creating a new statement for modules. This makes it easier to remember. It takes the module’s name.

The target_sources() (line 11) specifies which files are part of the module. First, a FILE_SET (line 13) states an MUI is being built using CXX_MODULES, an addition specifically for C++ modules. The FILES (line 14) indicates which file to use. The PRIVATE (line 15) brings the implementation unit file into the build.

The statement target_compile_features() (lines 6 and 10) specifies the C++ standard used. This is required when building on the command line but not in CLion.

Building the main file and any other non-module files is the same as using any library. You can either list the files in another target_sources() or include them in the add_executable() statement (line 5). The standard target_link_libraries() (line 7) associates the module with the target.

The last line (line 19) is used to build from the command line instead of CLion. It creates a build directory for the CMake files, populates it, builds it with verbose output to see what happens, and runs the executable. I added this so I don’t have to remember or type it each time it’s needed.

I won’t repeat the command lines or the CMake files for all the following examples unless the project in a chapter requires significant changes. Later chapters with a clowder of files and subdirectories will need expanded build details. (Are files that won’t build a glare of files?)

Module Deep Dive

Now that we’ve seen the basics of modules and some code to provide context, let’s look deeper into them.

Using a Module in Code

Using a module is simple, as we’ve seen earlier. It only requires an import declaration.

1 ```
2 import Aegean;
3 ```

The declaration brings the module Aegean into file scope, providing access to anything the module exports. The code using a module is an importer.

Module Segments and Fragements

Here’s a table listing the keywords, segments, and fragments of a Module Interface Unit and Module Implementation Unit.

Module Interface Unit Module Implementation Unit
module; module;
global module fragment global module fragment
(preprocessor commands only) (preprocessor commands only)
(export) import module name(:partition) import module name(:partition)
module purview module purview
export module name(:partition); module name(:partition);
export… export…
(implementation code) (implementation code)
module :private
private module fragment
(implementation code)

Each column lists the keywords and sections used in a Module Interface Unit (MIU) and a Module Implementation Unit. Let’s look at them in detail, and revisit those we’ve discussed before.

Module Naming

A module unit name and partition name follow the same rules as any other name in C++, except that a period may appear as a separator for readability. The period has no semantic purpose.

A colon separates the module unit and partition names. An implementation translation unit always uses just the module name, even when it is a partition. In a following section, we’ll see when only the partition name is necessary and when both names are required.

module; Starts the Global Module Fragment

The module; is an optional declaration that indicates this is a module. It must be the first line of code in the source, with only comments allowed before it. This declaration may be used in any module translation unit.

The purpose of module; is to begin the global module fragment, which ends at the export module or module declarations discussed in the next section. This fragment allows only pre-processor statements, primarily header inclusion statements. If a global module fragment is not needed, the module; declaration may be omitted.

An icon indicating this blurb contains a warning

Compilers may allow code other than precompiler statements to be present, but this is non-standard. As compilers comply with the standard, they may report errors in the future in place of warnings that some now emit.

Declarations (export) module name(:partition);

The export module name(:partition); and module name(:partition) declarations create a named module or module partition unit. They are used in the MIU and implementation module units, respectively. These declarations end the global module fragment and begin the module unit purview, which includes everything to the end of the translation unit.

The export version places the module in the global namespace, making it available to importers. The implementation version places the translation unit in the module. Here are examples of the primary MIU, non-partition implementation translation units, partition MIU, and partition implementation. Partitions are discussed in Partitions and Submodules

  • Primary MIU: export module Javanese;
  • Implementation: module Javanese;
  • Partition MIU: export module Javanese:Balinese;
  • Partition Implementation: Module implementation: module Javanese;

Declarations (export) import (module.name)(:partition)

The (export) import (module name)(:partition) and import (module.name)(:partition) declarations are used in the MIU and unit implementation files, respectively, to import modules into their namespace. The same statement allows importers to access a module. The export version exposes the module to the global namespace. The names must be the same as the name of the target module.

The export import declaration is used in an MIU to import a partition into the MIU and export it to other translation units. An MIU exports a partition using export import :partition; without the need to specify the module name.

1 `export import :Balinese;    // exports all from :Balinese partition`

export… or How Can I Export Thee? Let Me Count the Ways

Listing what can’t be exported from a module would be easier, but I’ll list both. The book provides examples of many allowed exports. We’ll start with what can’t be exported.

What Cannot Be Exported?

  • Macros (#define) (because they belong to the preprocessor)
  • Preprocessor Directives (#pragma once, #include)
  • Anonymous Namespaces

Exportable Entities in C++ Modules

  1. Functions
  • Regular functions
  • constexpr functions
  • inline functions
  1. Classes and Structs
  • Classes
  • Structs
  • Member functions
  • Static member variables
  1. Variables
  • const variables
  • constexpr variables
  • inline variables
  1. Enumerations
  • enum
  • enum class
  1. Type Aliases
  • using (type alias)
  • typedef
  1. Templates
  • Function templates
  • Class templates
  • Variable templates
  • Alias templates
  1. Concepts
  • concept definitions
  1. Namespaces
  • Entire named namespaces
  • Specific members inside a named namespace
  • Neither anonymous namespaces nor their content can be exported

Declaration module :private

This statement only applies to the primary MIU. It is a private fragment containing the implementation code for the module. A module unit with a private module fragment must be the only module unit in the module. This code does not change the Built Module Interface (BMI), so modifications do not require recompilation of the user code; only relinking is needed. It is hidden and may not be exported. [Rud - need to check some of these points with code.]

An icon indicating this blurb contains a warning

Neither GCC 14,2, Clang 19/20, nor MSVS 17.9 support private fragments. [Rud - try to test MS with godbolt.] A workaround is to create a module partition and not export it to the global namespace.

Module Linkage

Modules introduce new linkage definitions to support the encapsulation provided by modules.

Pre-Module Linkage

Before C++20 provided modules, two forms of linkage were defined: internal and external.

Internal Linkage
Symbols are limited to the translation unit where they are declared, a form of implementation hiding since other translation units cannot access them. These symbols are declared static or in an anonymous namespace.
External Linkage
Non-static functions and non-const global symbols are accessible by other translation units. External linkage is problematic because symbols in multiple translation units can collide, causing a linker error. Symbols also leak into the global scope, which may not be desirable.

C++20 Module Linkage

The main change is the requirement to use export to expose any symbols in a module to other translation units, which must, in turn, explicitly access them by importing the module. Within a module, non-exported symbols have module linkage so any translation unit in the module may access them. Module linkage improves encapsulation by hiding non-exported symbols. This linkage also reduces linking errors from name duplication. Use static and anonymous namespaces only when isolation from other translation units in the same module is necessary.“

Global Module
The global module bridges existing C++ code and modules. This module contains all the symbols in global module fragments. The pre-module internal and external linkage rules apply. You can’t declare symbols directly in a global module fragment, but they can be introduced by including a header file. Should the header contain static or anonymous namespaces, they are available to the translation unit. Code in a global module fragment is identical to code in a non-module translation unit.

The global module and global scope are nearly identical, and pragmatically, they are. The only distinction is that static and anonymous namespaces are considered part of the global module despite not being in the global scope.

Comparison of Header and Module Builds (WIP)

How do headers and modules compare when building an executable? Is one clearly better than the other?

Let’s look closely at the steps required to build a module and compare it with using headers. We’ll us the build to examine both processes. The name is long so it is reduced to just the initials in the diagrams.

Building with Headers

The compilation process using headers requires each header to be included in the source file which uses it. Diagrammatically it is:

Figure 7.1. Building with Headers
 1 abc.h      main.cpp    abc.h      abc.cpp
 2    │           │           │           │
 3    └─────┬─────┘           └─────┬─────┘ 
 4          v                       v
 5       main.o                  abc.o
 6          │                       │
 7          └────────────┬──────────┘
 8  9                       v
10                     meow

Here the headerabc.h is used by both main.cpp and abc.cpp. The header file must be read and processed twice by the compiler. That is two compilations and one linkage to build meow.

Consider what happens with an incremental build when the header or the implementation changes.

Header Change
A change to the header requires recompilation of both translation units and a link.
Implementation Change
A change to the implementation file requires recompiling that file and relinking.

Building with Modules

Building an application using modules is more complex. Diagrammatically building the module ABC,cppm with an implementation file abc.cpp is:

Figure 7.2. Building with Modules
 1      Abc.cppm        
 2   ┌────┴───┐         abc.cpp
 3   │        │           │  
 4   |        v           │  
 5   |     Abc.BMI ─┬─────┘ 
 6   │              │
 7   V              V
 8 Abc.o ─┬────── abc.o
 9        |               main.cpp
10        │                  │ 
11        │     Abc.BMI ─┬───┘ 
12        │              │ 
13        V              V   
14      abc.a ──┬───── main.o    
15 16              V
17            meow
18 
19 .BMI is ,gcm for g++ and .pcm for clang++

The first step is compiling the MIU Abc.cppm to produce the BMI and object files, which are then used in the subsequent steps. The BMI is used by abc.cpp to build its object file.

An icon indicating this blurb contains information

In writing this section, I examined the CMake build process closely, which resolved a question about whether the module object files should be linked or archived. CMake creates an archive, as shown in the diagram. Archiving will be used in the later chapters. I’ll use the term linkage to refer to archiving or linking. However, I later found that Clang discourage using archives with modules.

The two object files are linked to create an archive. Next, main.cpp is built using the BMI to generate an object file, which is linked with the archive to create the executable.

That is three compilations and two linkages to produce meow.

Again, let’s consider the steps for an incremental build if the MIU or the implementation changes.

MIU Change:
A change to the MIU recompiles the MIU, does an archive build, and performs a link.
Implementation Change
A change to the implementation recompiles the file, does an archive build, and performs a link.

Comparison Summary

It might seem like modules are worse off in terms of efficiency. The intent was to demonstrate using diagrams how the MIU, implementation files, and application files interact in a build, not directly comparing them with headers.

What is missing in the comparison?

  1. A header file library built with multiple implementation files would likely be archived, bringing it on par with modules in the number of linking steps.
  2. A change in a header file requires that every implementation file that uses the header be built. The build time becomes lengthy if an external header like the enormous <regex> is included.
  3. With modules, only implementation files that change their interface must be built along with the MIU. (They instigate the change to the MIU!)

In any non-trivial project, modules will be built more efficiently because they reduce the number of compilations. In fairness, the scanning process, as seen with the command-line builds of Aegean, does increase the compilation process.

Partitions and Submodules (WIP)

Comparison of Partitions and Submodules

Entities declared in a partition but not exported are only accessible within that partition and by its importers. Other module units, even within the same module, cannot access them unless they import the partition. Alternatively, a partition can be viewed as a subunit with its own module linkage.

Feature Module Partition Submodules
Encapsulation Hidden from external code but shared within the module Hidden from external code but not naturally shared within the module
Code Reuse Usable by multiple module units Requires manual inclusion (e.g., via a header or explicit linkage)
Faster Compilation Does not trigger module consumers’ recompilation Changes may require recompiling all users
Better Organization Keeps internals modular while maintaining cohesion Can lead to scattered internal dependencies
Module Dependency Management Naturally integrates with module build system Requires careful header and linkage management

A Partitioned Module (Javanese) (WIP)

Module Interface Unit: Javanese

Figure 9.1. Javanese.cppm
1 module;
2 
3 export module Javanese;
4 export import :Balinese;    // exports all from :Balinese
5 
6 export auto meow() -> void; // from javanese.cpp

Module Implementation Unit: Javanese

Figure 9.2. Javanese.cpp
1 module;
2 
3 #include <print>
4 
5 module Javanese;
6 
7 auto meow() -> void  {
8    std::println("Kucing jawa ngomong 'mbelong'");
9 }

Partition Module Interface Unit: Balinese

Figure 9.3. Balinese.cppm
1 module;
2 
3 export module Javanese:Balinese;
4 
5 export namespace Balinese {
6    auto meong() -> void;
7 }

Partition Module Implementation Unit: Balinese

Figure 9.4. Balinese.cpp
 1 module;
 2 
 3 #include <print>
 4 
 5 module Javanese:Balinese;
 6 
 7 namespace Balinese {
 8    auto meong() -> void {
 9       std::println("Balinese anak kucing says 'meong'");
10    }
11 }

User Code: main

1 import Javanese;
2 
3 auto main() -> int {
4    meow();
5    Balinese::meong();
6 
7    return 0;
8 }

Building Javanese Partition Module

Building Javanese with GCC

Figure 9.5. GCC Command Lines for Javanese
1 + g++ -std=c++23 -fmodules-ts -c -x c++ Balinese.cppm -o bin/Balinese.o
2 + g++ -std=c++23 -fmodules-ts -c -x c++ Javanese.cppm -o bin/Javanese.o
3 + g++ -std=c++23 -fmodules-ts -c javanese.cpp -o bin/javanese.o
4 + g++ -std=c++23 -fmodules-ts -c balinese.cpp -o bin/balinese.o
5 + ar rcs bin/libJavanese.a bin/Javanese.o bin/Balinese.o bin/javanese.o bin/balinese.o
6 + g++ -std=c++23 -fmodules-ts -c main.cpp -o bin/main.o
7 + g++ -std=c++23 -fmodules-ts bin/main.o -Lbin -lJavanese -o bin/main

Building Javanese with Clang

Figure 9.6. Clang Command Lines for Javanese
1 + clang++ -std=c++23 --precompile Balinese.cppm -o bin/Javanese-Balinese.pcm
2 + clang++ -std=c++23 --precompile -fprebuilt-module-path=bin Javanese.cppm -o bin/Javanese.pcm
3 + clang++ -std=c++23 -c -fprebuilt-module-path=bin Javanese.cppm -o bin/Javanese.o
4 + clang++ -std=c++23 -c -fprebuilt-module-path=bin javanese.cpp -o bin/javanese.o
5 + clang++ -std=c++23 -c -fprebuilt-module-path=bin balinese.cpp -o bin/balinese.o
6 + clang++ -std=c++23 -r bin/Javanese.o bin/javanese.o bin/balinese.o -o bin/Javanese.Module.o
7 + clang++ -std=c++23 -c -fprebuilt-module-path=bin main.cpp -o bin/main.o
8 + clang++ -std=c++23 bin/Javanese.Module.o bin/main.o -o bin/main

Building with CMake

Here is the script. See Parent Directory CMakelist.txt for the parent directory file. Section : Parent Directory CMakelist.txt

Figure 9.7. Javanese Subdirectory CMakelists.txt
 1 cmake_minimum_required(VERSION 3.30.5)
 2 project(Javanese LANGUAGES CXX)
 3 set(module_name javanese)
 4 
 5 add_executable(${PROJECT_NAME} main.cpp)
 6 target_link_libraries(${PROJECT_NAME} ${module_name})
 7 
 8 add_library(${module_name})
 9 target_compile_features(${module_name} PUBLIC cxx_std_23)
10 target_sources(${module_name}
11                PUBLIC
12                FILE_SET cxx_modules TYPE CXX_MODULES
13                FILES
14                     Balinese.cppm
15                     ${PROJECT_NAME}.cppm
16                PRIVATE
17                     ${module_name}.cpp
18                     balinese.cpp
19 )

Private Module Fragement (Sphynx) (WIP)

Figure 10.1. Aegean.Private.cppm
 1 module;
 2 
 3 #include <format>
 4 #include <print>
 5 #include <regex>
 6 
 7 export module Sphynx;
 8 
 9 using std::println, std::print;
10 
11 export class Sphynx; //
12 export auto meow() -> void; //
13 
14 module :private;
15 
16 class Sphynx {
17 private:
18    int value{};
19 
20 public:
21    auto print() -> void {
22       meow();
23    }
24 };
25 
26 auto meow() -> void {
27    println("Sphynx cat breed in Canada says 'meow, eh!'");
28 }
Figure 10.2. main.cpp
1 import Sphynx;
2 
3 auto main() -> int {
4    Sphynx sphynx;
5    sphynx.print();
6 
7    return 0;
8 }
Figure 10.3. Clang error when accessing private code
1 error: missing '#include'; 'Sphynx' must be defined before it is used
2     4 |    Sphynx sphynx;
3       |           ^
4 note: definition here is not reachable
5    16 | class Sphynx {
6       |       ^
Figure 10.4. GCc error from private fragment
1 sorry, unimplemented: private module fragment
2    15 | module :private;
3       | ^~~~~~

Resources

I found these resources helpful in studying modules for this book and, hopefully, my CppNorth 2025 presentation.

C++ Standard. Module https://eel.is/c++draft/module

Richard Smith. Merging Modules https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1103r3.pdf

Ben Boeckel. Beyond Compilation Databases to Support C++ Modules: Build Databases,

  • Slides: https://github.com/CppCon/CppCon2024/blob/main/Presentations/Beyond_Compilation_Databases_to_Support_Cpp_Modules.pdf
  • Video: https://www.youtube.com/watch?v=GUqs_CM7K_0&pp=ygUTY3BwY29uIDIwMjQgYm9lY2tlbA%3D%3D

Bill Hoffman. import std in CMake 3.30, https://www.kitware.com/import-std-in-cmake-3-30/

Bill Hoffman. import CMake; // Mastering C++ Modules,

  • Slides: https://github.com/CppCon/CppCon2024/blob/main/Presentations/import_CMake_Mastering_Cpp_Modules.pdf

https://github.com/CppCon/CppCon2024/blob/main/Presentations/import_CMake_Mastering_Cpp_Modules.pdf

  • Video: https://www.youtube.com/watch?v=7WK42YSfE9s&t=2960s&pp=ygUiaW1wb3J0X0NNYWtlX01hc3RlcmluZ19DcHBfTW9kdWxlcw%3D%3D

Craig Scott. C++20 Modules, CMake, And Shared Libraries, https://crascit.com/2024/04/04/cxx-modules-cmake-shared-libraries/

LLVM. Standard C++ Modules, https://clang.llvm.org/docs/StandardCPlusPlusModules.html

Microsoft. Compare header units, modules, and precompiled headers, https://learn.microsoft.com/en-us/cpp/build/compare-inclusion-methods?view=msvc-170

Luis_Caro_Campos. C++20 Modules: The Packaging and Binary Redistribution Story,

  • Slides: https://github.com/CppCon/CppCon2023/blob/main/Presentations/Cpp_Modules__the_packaging_story_Luis_Caro_Campos.pdf
  • Video: https://www.youtube.com/watch?v=-p9lvvV8F-w&pp=ygUgQ3BwX01vZHVsZXM6IFRoZSBQYWNrYWdpbmcgU3Rvcnk%3D

Adreas Weis. Cpp Modules: Getting Started Today,

  • Slides: https://github.com/CppCon/CppCon2023/blob/main/Presentations/cpp_modules_getting_started_today.pdf
  • Video: https://www.youtube.com/watch?v=_x9K9_q2ZXE&pp=ygURY3BwY29uIGltcG9ydCBzdGQ%3D

vector{ true, true, false };. Understanding C++ Modules: Part 1: Hello Modules, and Module Units, https://vector-of-bool.github.io/2019/03/10/modules-1.html

About the Author

FORTRAN IV was my first language in 1968. I attended a one-day-a-week six-week seminar at the State University of New York/Buffalo to learn the language and program submission to the IBM 704. I soon became a full-time computer operator at the university, working on a CDC 6400 and finishing my degree there. During that period, I was exposed to multiple languages: Algol, SNOBOL, Lisp (a lot of silly insipid parenthesis), and Pascal. I saw copies of the original Pascal compiler code.

I next worked at the University of Rochester Laboratory for Laser Energetics, where I trained and worked with Forth. Unfortunately, that position went away, and I moved to Houston. My positions there exposed me to assembly language, more Fortran and Forth, and PL/M and C for Intel embedded systems.

I started using C++ with the 1990 release of Turbo C++, switching to MSVC shortly after and until my retirement in 2006. With the demise of Windows XP, I switched to Linux using GCC and, more recently, Clang.

I’ve used C++ in robotics competitions for NASA and the National Institute of Science and Technology, winning some money but not the big prizes. They were great exercises.

In 2022, I snuck, by invitation, into the Toronto CppNorth convention for one evening. This sparked an interest in speaking, which I did at CppNorth and CppCon in ’23 and again in Toronto in ’24. This study of modules is proposed for the 2025 CppNorth conference.

I’ve also written for programming magazines in the ’90s, Hackaday.com in the mid-2010s, and Medium.com for the last few years.