C++20 modules
A header is text inserted by the preprocessor, and everything it declares is visible to every file that includes it. A module is compiled once into a binary interface, and import reads that interface instead of the text.
The gain is isolation and speed: a module does not leak macros, a change to its body does not recompile its importers, and the compiler does not parse the same declarations again. GCC implements the feature behind -fmodules-ts, which is why every command in this lesson carries the flag.
A module can also export a namespace and the names inside it, so an importer sees the interface it expects. The build order and the cache are the practical cost, and a build system that understands imports is the practical requirement.
The module interface can also re-export a name from another module, which is how a library exposes one entry point and hides the parts. That line is the module equivalent of including a header in a header.
export module and import
The interface unit declares export module math; and marks each declaration with export. The importer writes import math; and sees only the exported names, not the includes and helpers inside the module.
The module is compiled first, which writes gcm.cache/math.gcm, and the program is compiled against that file. The run prints 49, and the listing shows the cache the compiler created.
The build order is the new dependency: the module before its importers. A build system derives it from the import graph, which is why CMake 3.28 and later know how to handle modules.
math.cppm
export module math;
export int square(int value)
{
return value * value;
}
main.cpp
import math;
#include <iostream>
int main()
{
std::cout << square(7) << '\n';
}
g++ -std=c++23 -Wall -Wextra -fmodules-ts -c math.cppm -o math.o
g++ -std=c++23 -Wall -Wextra -fmodules-ts main.cpp math.o -o app
./app
ls gcm.cache
49
math.gcm
A class that crosses the boundary
An exported class works like an exported function: its declaration and its inline members go into the interface, and the importer can create objects and call them without the source.
The program exports a counter with two inline methods, imports it and adds two values. The output is 6, and the importer never sees the module's source file.
The class is defined in the module interface, so its inline functions are compiled into every importer, which is the same rule as an inline function in a header. Moving the bodies into a module implementation unit is the way to keep them out of the importers.
counter.cppm
export module counter;
export class counter
{
public:
void add(int value) { value_ += value; }
int get() const { return value_; }
private:
int value_ = 0;
};
main.cpp
import counter;
#include <iostream>
int main()
{
counter c;
c.add(2);
c.add(4);
std::cout << c.get() << '\n';
}
g++ -std=c++23 -Wall -Wextra -fmodules-ts -c counter.cppm -o counter.o
g++ -std=c++23 -Wall -Wextra -fmodules-ts main.cpp counter.o -o app
./app
6
Two modules in one program
A program can import several modules, and each one is compiled to its own interface. The modules do not see each other unless one imports the other, which is the isolation the header model lacks.
The program builds two modules with different names and imports both in main. The squares and the triples are printed as 9 and 27, and neither module includes the other's header.
Two modules may export the same name without a clash, because a name is qualified by its module. That is the reason a large codebase can stop renaming functions to avoid the one-definition problem of headers.
square.cppm
export module square;
export int square(int value)
{
return value * value;
}
triple.cppm
export module triple;
export int triple(int value)
{
return value * 3;
}
main.cpp
import square;
import triple;
#include <iostream>
int main()
{
std::cout << square(3) << ' ' << triple(9) << '\n';
}
g++ -std=c++23 -Wall -Wextra -fmodules-ts -c square.cppm -o square.o
g++ -std=c++23 -Wall -Wextra -fmodules-ts -c triple.cppm -o triple.o
g++ -std=c++23 -Wall -Wextra -fmodules-ts main.cpp square.o triple.o -o app
./app
9 27
The module that was never built
An import needs the compiled interface. Compiling the importer first leaves the compiler looking for gcm.cache/<name>.gcm, and the message says the file is missing and that imports must be built first.
The program compiles main.cpp alone. The error names the module, the cache file and the rule, and it stops before the compiler reaches the includes.
The fix is the build order, or a build system that reads the imports. -fmodules-ts does not do it for you: the flag enables the feature and the cache, and the order remains the project's job.
math.cppm
export module math;
export int square(int value)
{
return value * value;
}
main.cpp
import math;
int main()
{
return square(7);
}
g++ -std=c++23 -Wall -Wextra -fmodules-ts main.cpp -o app
In module imported at main.cpp:1:1:
math: error: failed to read compiled module: No such file or directory
math: note: compiled module file is 'gcm.cache/math.gcm'
math: note: imports must be built before being imported
math: fatal error: returning to the gate for a mechanical issue
compilation terminated.
Exercises
- Add a cube function to the math module and print it from main.
- Move the counter's method bodies into a module implementation unit and rebuild.
- Build the two-module program in the wrong order and read the error.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
export module math;declare?Answer
The start of the module interface unit named math
The line names the module and marks the file as its interface.
-
What does
import math;read?Answer
The compiled module interface, not the source text
The compiler loads the binary interface from gcm.cache, which is why the module must be built first.
-
Which file does g++ write for a module interface?
Answer
gcm.cache/math.gcmThe cache holds one compiled interface per module, and the note in the error names the path.
-
What does the line
export module math;declare?Answer
The start of the module interface unit named math
The line names the module and marks the file as its interface unit.
-
What does
import math;read in an importing source file?Answer
The compiled module interface, not the source text
The compiler loads the binary interface from gcm.cache, which is why the module must be built first.
-
A program imports two modules. What must the build do?
Answer
Compile each module before the file that imports it
Each import is resolved from the module's compiled interface, so the module has to exist first.
-
Why must a module be compiled before its importers?
Answer
Because the importer reads its compiled interface
The import is resolved from the cache, so the interface has to exist when the importer is compiled.
-
What does the compiler report when the module was never built?
Answer
That the compiled module file is missing and imports must be built first
The error names the module and the gcm.cache path it tried to read.
-
What does
-fmodules-tsdo?Answer
Enables the modules implementation in GCC
The flag turns the feature on; the build order is still the project's responsibility.
-
What advantage do modules have over headers for a large project?
Answer
A module is compiled once and importers do not re-parse its declarations
The binary interface replaces textual inclusion, which saves parsing and stops macro leakage.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev