modules
C++20 modules replace the traditional header-file-plus-preprocessor-include mechanism with a new compilation unit management system. Modules eliminate repeated text expansion of headers, shorten build times, prevent macro pollution, and give explicit control over what is exported.
Syntax
// --- Module interface file (.cppm or .ixx) ---
// Declare the module name; "export module" makes this an interface unit
export module mylib;
// Add "export" to declarations and definitions visible outside the module
export int add(int a, int b) {
return a + b;
}
// Identifiers without "export" are module-private
int helper(int x) {
return x * 2;
}
// Export block: export multiple declarations at once
export {
int multiply(int a, int b);
double divide(double a, double b);
}
// --- Module implementation file (.cpp) ---
// Declare the same module name without "export" to create an implementation unit
module mylib;
int multiply(int a, int b) { return a * b; }
double divide(double a, double b) { return a / b; }
// --- Consumer file (.cpp) ---
// Use "import" to load the module (no #include needed)
import mylib;
int main() {
int result = add(3, 4);
// helper(5); // compile error: not exported
return 0;
}
Syntax Reference
| Syntax / Concept | Description |
|---|---|
| export module name; | Declares a module interface unit. Place this at the top of the file that defines the module's public API. |
| module name; | Declares a module implementation unit. Written without "export". The function/class implementations go here. |
| import name; | Loads a module. Unlike #include, it references pre-compiled module information, making builds faster. |
| export declaration; | Makes a function, class, variable, or type alias visible outside the module. Identifiers without export are module-private. |
| export { ... } | Export block. Exports multiple declarations at once instead of prefixing each with export. |
| module :private; | Private module fragment. Everything after this line is completely invisible outside the module. |
| import <header>; | Imports a standard library header in module form. Implementation-defined, but more efficient than #include. |
Sample Code
An organization management library modeled on Yakuza (Like a Dragon) characters, implemented as a module. The project uses three files: an interface unit, an implementation unit, and a consumer.
First, create the interface unit (.cppm or .ixx) that defines the module's public API.
yakuza_org.cppm
// yakuza_org.cppm
// Organization management module using Yakuza characters.
// This file defines the module's public interface.
export module yakuza_org;
#include <string>
#include <vector>
// Exported struct: Member
export struct Member {
std::string name;
std::string role;
int strength;
};
// Exported class: YakuzaOrg
export class YakuzaOrg {
public:
explicit YakuzaOrg(const std::string& orgName);
void addMember(const Member& m);
void showAll() const;
Member strongest() const;
private:
std::string orgName_;
std::vector<Member> members_;
};
// Exported utility function
export void printMember(const Member& m);
// Module-private helper (not exported)
std::string formatStrength(int strength);
Next, create the module implementation unit (.cpp) that contains the implementations declared in the interface.
yakuza_org.cpp
// yakuza_org.cpp
// Implementation unit for the yakuza_org module.
module yakuza_org;
#include <iostream>
#include <string>
#include <stdexcept>
// Module-private helper implementation
std::string formatStrength(int strength) {
if (strength >= 90) return "[Legend]";
if (strength >= 70) return "[Executive]";
if (strength >= 50) return "[Mid-level]";
return "[General]";
}
YakuzaOrg::YakuzaOrg(const std::string& orgName) : orgName_(orgName) {}
void YakuzaOrg::addMember(const Member& m) {
members_.push_back(m);
}
void YakuzaOrg::showAll() const {
std::cout << "=== " << orgName_ << " Members ===" << std::endl;
for (int i = 0; i < (int)members_.size(); i++) {
printMember(members_[i]);
}
std::cout << std::endl;
}
Member YakuzaOrg::strongest() const {
if (members_.empty()) {
throw std::runtime_error("No members registered.");
}
Member best = members_[0];
for (int i = 1; i < (int)members_.size(); i++) {
if (members_[i].strength > best.strength) {
best = members_[i];
}
}
return best;
}
void printMember(const Member& m) {
std::cout << " " << formatStrength(m.strength)
<< " " << m.name
<< " (" << m.role << ")"
<< " Strength: " << m.strength << std::endl;
}
Finally, create the consumer file that imports the module and uses it.
main.cpp
// main.cpp
// Imports yakuza_org and uses the exported types and functions.
import yakuza_org;
#include <iostream>
int main() {
YakuzaOrg tojo("Tojo Clan");
tojo.addMember({ "Kiryu Kazuma", "Former 4th Chairman aide", 98 });
tojo.addMember({ "Majima Goro", "Majima Family patriarch", 95 });
tojo.addMember({ "Shimano Futoshi", "Shimano Family patriarch", 80 });
tojo.addMember({ "Akiyama Shun", "Sky Finance chairman", 72 });
tojo.addMember({ "Nishikiyama Akira","Nishikiyama Family patriarch", 85 });
tojo.showAll();
Member top = tojo.strongest();
std::cout << "=== Strongest Member ===" << std::endl;
printMember(top);
std::cout << std::endl;
// formatStrength(98); // compile error: module-private, not visible here
return 0;
}
g++ -std=c++20 -fmodules-ts -x c++-module yakuza_org.cppm -c -o yakuza_org.o g++ -std=c++20 -fmodules-ts -c yakuza_org.cpp -o yakuza_org_impl.o g++ -std=c++20 -fmodules-ts main.cpp yakuza_org.o yakuza_org_impl.o -o yakuza_org_demo ./yakuza_org_demo === Tojo Clan Members === [Legend] Kiryu Kazuma (Former 4th Chairman aide) Strength: 98 [Legend] Majima Goro (Majima Family patriarch) Strength: 95 [Executive] Shimano Futoshi (Shimano Family patriarch) Strength: 80 [Mid-level] Akiyama Shun (Sky Finance chairman) Strength: 72 [Executive] Nishikiyama Akira (Nishikiyama Family patriarch) Strength: 85 === Strongest Member === [Legend] Kiryu Kazuma (Former 4th Chairman aide) Strength: 98
Common Mistake 1: Mixing #include and module declarations incorrectly
Placing #include after export module can cause warnings or errors depending on the compiler. Use a global module fragment (module;) before the export module line to place legacy #include directives.
// NG (may error): #include placed after export module declaration export module mylib; #include <string> // may cause issues on some compilers
OK: place #include in the global module fragment before the module declaration.
// OK: module; // global module fragment begins here #include <string> #include <vector> export module mylib;
Common Mistake 2: Wrong compilation order causes "module not found"
The module interface unit must be compiled first to generate the BMI (Binary Module Interface). Compiling main.cpp before the interface unit produces a "module not found" error.
// NG: compiling main.cpp before the interface unit // g++ -std=c++20 -fmodules-ts main.cpp -o demo ← error: yakuza_org not found
OK: compile in the order — interface unit → implementation unit → consumer.
// OK: correct build order // 1. Pre-compile the interface unit // g++ -std=c++20 -fmodules-ts -x c++-module yakuza_org.cppm -c -o yakuza_org.o // 2. Compile the implementation unit // g++ -std=c++20 -fmodules-ts -c yakuza_org.cpp -o yakuza_org_impl.o // 3. Compile and link main.cpp // g++ -std=c++20 -fmodules-ts main.cpp yakuza_org.o yakuza_org_impl.o -o demo
Overview
C++20 modules replace the long-standing header-file and preprocessor text-inclusion mechanism with a new compilation unit management system. The traditional approach repeatedly expanded and parsed header text on each inclusion, inflating build times and carrying the risk of macro side-effects across files. Modules pre-compile the interface once into a BMI (Binary Module Interface) and reuse it, making them effective for shortening builds in large projects. Only declarations marked with export are visible outside the module; everything else is module-private, so encapsulation is enforced at the compiler level. A file with export module name; is the interface unit; a file with module name; is the implementation unit — a structure analogous to the traditional header/source split. Build flags vary by compiler: GCC uses -fmodules-ts, Clang uses -fmodules. The import <iostream>; syntax for importing standard library headers in module form is part of the specification, and as compiler support matures it is expected to eventually replace #include entirely.
If you find any errors or copyright issues, please contact us.