Skip to the content.

How do modules change C++?

The rollout of C++20 this year brings a host of new features that will add to and change the way we write C++ code. However, the introduction of modules in particular will bring changes affecting some of the key aspects of C++, ranging from how code is parsed all the way to how projects are consumed.

So what are C++ modules? At the highest level, modules are a new compilation model for C++, and a new way to organize C++ projects. In order to better understand what these changes are and what they mean, let’s take a look at two examples – one from the current C++ compilation model and one from the new module compilation model.


🔗 Textual Inclusion Compilation Model

C++’s current compilation model is akin to C’s compilation model. The process is broken into three steps:

  1. Preprocessing
  2. Compilation
  3. Linking

In order to understand the preprocessing step, let’s first look at the basic organization of C++ projects. C++ projects are organized into header files and source files. Header files, ending with .hpp, hold declarations of functions, classes, structs, etc. In the example below, hello.hpp holds the declaration of the function say_hello(). Source files, ending with .cpp, hold the implementations of any entities defined in the headers. So the hello.cpp example below holds the implementation of the say_hello() function, which returns “Hello World!” when the function is called.


Header file (.hpp) - holds declarations

#pragma once

const char* say_hello();


Source file (.cpp) - holds implementation

#include "hello.hpp"

const char* say_hello(){
    return "Hello World!";
}


main.cpp - includes hello.hpp and calls say_hello

#include "hello.hpp"
#include <iostream>

int main(){
    std::cout << say_hello() << std::endl;
}


During preprocessing, header files and source files are organized into translation units. Each translation unit is made of a source file along with any headers or other source files that are #included. In the example above, the “hello” translation unit would consist of the hello.cpp source file, along with the hello.hpp header file.

Translation units are assembled during preprocessing using textual inclusion. This means anytime the preprocessor sees an #include, it takes all the text from the file that is being included, and sticks it in the source file in place of the #include statement.

After the translation units are assembled, the next step is compilation. In this step, each translation unit is compiled into an object file. Then, in the final step, the compiled translation units are linked together to create the executable C++ program.

Pros

Textual inclusion allows for parallel compilation of all translation units. It doesn’t matter if the same header file is included in multiple source files – since all the text from the header is placed into each of the source files, they can be compiled in any order.

Cons

If the same header is included in multiple source files, then that exact same header file has to be compiled multiple times. The bigger your projects become/the more places you include the same header file, the more this is going to slow down your compile time. In addition, the header file organization system causes a number of other problems due to the fact that it is not sandboxed, it is include order dependant, it allows cyclic dependencies, and it is not safe against macros.


🔗 Module Compilation Model

So, how are modules different? Instead of header and source files, modules are split into Module Interface Units and Module Implementation Units, which follow the same basic idea of splitting up declarations from their implementations.

In the module interface unit in the example below, export module hello; defines the module name to be hello. Anything exported in this file will be available from the hello module. The first line in the module implementation unit, module hello;, indicates that it is providing the implementation for the hello module, and defines the say_hello() function to return “Hello World!” when called. The main function then imports the hello module, and imports the iostream header as a module, and then calls the say_hello() function.

(Note: using the import keyword with existing headers, such as iostream, will treat them like modules. This is intended to smooth out the transition to modules.)


Module Interface Unit - describes which entities are exported from the module

export module hello;

export const char* say_hello();


Module Implementation Unit - defines implementation of entities exported in the interface unit

module hello;

const char* say_hello(){
    return "Hello World!";
}


main.cpp - includes hello.hpp and calls say_hello

import hello;
import <iostream>;

int main() {
    std::cout << say_hello() << std::endl;
}


So far, other than some syntactic differences, module organization seems pretty similar to the header/source file organization. But here’s where modules diverge: instead of textually including header and source files into a single translation unit, module interface units are their own translation unit. As a result, these module interface translation units must be precompiled before preprocessing occurs.

This means we have to add a precompile step to the beginning of our compilation process in which our module interface units are compiled. Then, in the preprocessing step, instead of textually including headers, the compiler resolves imports by finding all the relevant, precompiled module interface units. Then, in the compilation step, the module implementation units and precompiled module interface unit are compiled into object files to form the module. Finally, everything is linked together to create the executable C++ program.

Pros

All module interface units are precompiled only once, regardless of how many source files import that module. The build time speedup that results is one of the major benefits of modules. In addition, modules solve a lot of the problems caused by headers that were listed in the previous section. (Info on how modules solve these problems can be found in the Modules Are Coming talk by Bryce Adelstein)

Cons

Not a major con, but a slight downside is that compilation of all translation units is not completely independent/parallelizable anymore. Module interface units are required to be compiled first.


🔗 Results

C++ Modules introduce a new organization and compilation model in order to achieve faster build times. As an added bonus, they provide a few other benefits, such as better encapsulation and disallowing cyclic dependencies. However, the full impact will most likely not be seen for quite a while, as it will take time, effort, and adjustment for developers and teams to switch over and adopt the use of modules.


🔗 Resources

More info on modules:

If you’d like to try out modules for yourself, check out these (experimental) implementations:

🔗 Comments? Questions?

Jump on the twitter thread below to chat!! ☎️ ☎️ ☎️