export module, import, and Module Partitions
C++20 modules replace the header/source split with a new two-file model: a module interface file that declares what the module exports, and an optional module implementation file that contains definitions. Once you understand the handful of new keywords — export module, import, export import — and the two structural tools (submodules and partitions), you can express any library organisation that headers allowed, plus several that headers make awkward.
Module interface files
A module interface file conventionally uses the .cppm extension (check your compiler documentation — this is not standardised). It opens with a named module declaration: export module name;. Everything from that line to the end of the file is the module purview. Only entities explicitly marked with export are visible to importers; everything else is module-private. The set of all exported entities constitutes the module interface.
Module names can contain dots (e.g., mycompany.datamodel.core) but cannot start or end with a dot, and cannot contain consecutive dots. You can export individual declarations, entire namespaces, or groups of declarations wrapped in an export block.
// Person.cppm — module interface file
export module person; // named module declaration
import std; // import declarations come first
// Export a single class
export class Person
{
public:
Person(std::string firstName, std::string lastName);
const std::string& getFirstName() const;
const std::string& getLastName() const;
private:
std::string m_firstName;
std::string m_lastName;
};// Export an entire namespace — all contents are automatically exported
export module datamodel;
import std;
export namespace DataModel
{
class Person { /* ... */ };
class Address { /* ... */ };
using Persons = std::vector<Person>;
}// Export block — export a group of declarations at once
export module datamodel;
import std;
export
{
namespace DataModel
{
class Person { /* ... */ };
class Address { /* ... */ };
using Persons = std::vector<Person>;
}
}
// Also legal: export individual using declarations
export using std::string; // re-exports string from this module
export import <vector>; // re-exports a header unitImporting a module
Any source file can import a module with an import declaration. Import declarations must appear after the module declaration (if any) but before any other declarations. The module is compiled once to a binary format; the compiler reuses that binary every time the module is imported — in contrast to headers, which are re-parsed from text on every include.
// test.cpp
import person; // import the person module
import std; // also needed — see "visibility vs. reachability" below
int main()
{
Person p { "Kole", "Webb" };
std::println("{}, {}", p.getLastName(), p.getFirstName());
}Module implementation files
A module implementation file uses a plain .cpp extension and opens with module name; (no export keyword). Implementation files cannot export anything — only module interface files can. The implementation file implicitly inherits all imports declared in the module interface file for the same module; there is no need to repeat import std; if the interface already imports it.
Person.cppm — interface
export module person;
import std;
export class Person
{
public:
Person(std::string first,
std::string last);
const std::string& getFirstName() const;
const std::string& getLastName() const;
private:
std::string m_firstName;
std::string m_lastName;
};Person.cpp — implementation
module person; // no export
// std::string is reachable via
// inherited import from interface
using namespace std;
Person::Person(string first, string last)
: m_firstName { move(first) }
, m_lastName { move(last) } {}
const string& Person::getFirstName() const
{ return m_firstName; }
const string& Person::getLastName() const
{ return m_lastName; }import declarations in both interface and implementation files must appear at the top — after the module declaration but before any other declarations. Implementation files cannot re-export; only interface files can export.Splitting interface from implementation in a single file
With modules, the module interface consists of class definitions and function prototypes — not implementations. This is unlike headers, where member functions defined inside a class body are implicitly inline and trigger recompilation of every including file when they change. In a module interface file, changing the implementation of a member function does not require recompilation of module consumers, as long as the interface (function signature, class layout) is unchanged. The only exceptions are inline functions and template definitions, which the compiler must see in full.
You can keep everything in a single .cppm file while still separating the class definition (prototypes only) from the implementations:
// person.cppm — single file, interface and implementation separated within it
export module person;
import std;
// Class definition — this IS the interface
export class Person
{
public:
Person(std::string firstName, std::string lastName);
const std::string& getFirstName() const;
const std::string& getLastName() const;
private:
std::string m_firstName;
std::string m_lastName;
};
// Implementations follow — NOT part of the module interface
Person::Person(std::string first, std::string last)
: m_firstName { std::move(first) }, m_lastName { std::move(last) } {}
const std::string& Person::getFirstName() const { return m_firstName; }
const std::string& Person::getLastName() const { return m_lastName; }Visibility vs. reachability
When test.cpp imports person, it does not inherit person's own imports. The std import lives inside the person module; it is not re-exported to consumers. The C++ standard distinguishes two states: visible and reachable.
An entity is visible when you can refer to it by name. An entity is reachable when the compiler knows its full definition — even if you cannot name it directly. When you import person, std::string becomes reachable (the compiler knows its members) but not visible (you cannot write std::string str; without an explicit import).
Does NOT compile
import person;
// std not imported here
int main()
{
// Error: std::string not VISIBLE
std::string str;
}Compiles fine — reachable
import person;
// std not imported here
int main()
{
Person p { "Kole", "Webb" };
// OK: getLastName() returns std::string
// string is REACHABLE via person
const auto& last = p.getLastName();
// OK: length() is visible on reachable type
auto len = last.length();
}The rule: to use a type by name in a file, you must have an import that makes it visible in that file. To call member functions on a returned value whose type is reachable, no extra import is needed. Add import std; to test.cpp if you want to name std::string directly.
Submodules — dot syntax for hierarchies
The C++ standard does not define "submodule" as a concept, but dot-separated names create a conventional hierarchy that users can import selectively. A top-level module (e.g., datamodel) can aggregate submodules (e.g., datamodel.person, datamodel.address) by exporting their imports. Clients then choose: import everything with import datamodel;, or import only what they need.
// datamodel.person.cppm — submodule interface
export module datamodel.person;
export namespace DataModel { class Person { /* ... */ }; }
// datamodel.address.cppm — submodule interface
export module datamodel.address;
export namespace DataModel { class Address { /* ... */ }; }
// datamodel.cppm — top-level module aggregates submodules
export module datamodel;
export import datamodel.person; // import AND re-export
export import datamodel.address;
import std;
export namespace DataModel { using Persons = std::vector<Person>; }// Client options: import datamodel; // everything: Person, Address, Persons import datamodel.address; // just Address (if it's a stable dependency)
Module partitions — internal structure with colon syntax
Module partitions let you split a module's implementation across multiple files without exposing that structure to users. The key difference from submodules: partitions are internal; users cannot import individual partitions. The primary module interface file (the one with export module name;) must ultimately export every partition whose exports it wants visible.
A partition name uses a colon separator: export module datamodel:person; declares an interface partition. Within the module, import a partition with just the colon-prefixed name: import :person;. Never prefix with the module name — that would be circular or illegal.
// datamodel.person.cppm — interface partition
export module datamodel:person; // colon, not dot
export namespace DataModel { class Person { /* ... */ }; }
// datamodel.address.cppm — interface partition
export module datamodel:address;
export namespace DataModel
{
class Address
{
public:
Address();
/* ... */
};
}// datamodel.cppm — primary module interface file
export module datamodel; // primary declaration (no colon)
export import :person; // import partition AND re-export its exports
export import :address;
import std;
export namespace DataModel { using Persons = std::vector<Person>; }// Client — can only import the whole module import datamodel; // OK // import datamodel:person; // Error: partitions are internal
| Submodules (dot) | Partitions (colon) | |
|---|---|---|
| Visible to users? | Yes — can import selectively | No — internal only |
| Syntax | export module mod.sub; | export module mod:part; |
| Import within module | import mod.sub; | import :part; |
| Import by users | import mod; or import mod.sub; | import mod; (whole module only) |
| Use case | Stable sub-APIs users might depend on separately | Splitting large modules internally |
Implementation partitions — shared private helpers
An implementation partition is declared without export (in a plain .cpp file) and exists purely for sharing helper code between multiple module implementation files. It cannot be exported. This is the right place for internal utility functions that several implementation files need but that should never be part of the public API.
// math_helpers.cpp — implementation partition (no export keyword)
module math:details;
double someHelperFunction(double a) { return /* ... */; }
// math.cpp — module implementation file
module math;
import :details; // import the implementation partition
// now has access to someHelperFunction
double Math::superLog(double z, double b) { return someHelperFunction(z); }
double Math::lerchZeta(double l, double a, double s) { return /* ... */; }Private module fragment
A private module fragment lets you keep the pimpl idiom in a single file. Add module :private; inside the primary interface file; everything after that line is hidden from all module consumers. Types defined in the private fragment are unknown to consumers — their existence is a compile-time secret.
// adder.cppm — pimpl in one file using private module fragment
export module adder;
import std;
export class Adder
{
public:
Adder();
virtual ~Adder();
int add(int a, int b) const;
private:
class Impl; // forward declaration only
std::unique_ptr<Impl> m_impl;
};
module :private; // ← everything below is hidden from consumers
class Adder::Impl
{
public:
~Impl() { std::println("Destructor of Adder::Impl"); }
int add(int a, int b) const { return a + b; }
};
Adder::Adder() : m_impl { std::make_unique<Impl>() } {}
Adder::~Adder() {}
int Adder::add(int a, int b) const { return m_impl->add(a, b); }Header units and importable standard library headers
For third-party or legacy headers you cannot convert to modules, C++20 allows importing them as header units. The compiler converts the header to a module-like binary on first use, improving subsequent builds. Unlike real modules, macros from the header become visible to the importer; this can cause the same macro-pollution problems as #include.
All C++ standard library headers (e.g., <vector>, <string>) are importable headers and can be used as header units. Starting with C++23, prefer the named module std instead — a single import gives you the entire standard library, macro-free.
// Header units — legacy/third-party headers you cannot convert import "include/person.h"; // relative path import <person.h>; // search system include directories // Importable standard library headers (C++20) import <vector>; import <string>; import <format>; // Preferred C++23: named module for the entire standard library import std; // everything in std:: (no macros) import std.compat; // std:: + C functions in global namespace (legacy interop)