Disclaimer: this cheat sheet is generated by Codex, based on BrickSim, an open-source project that uses this setup in the real world.
Pragmatic patterns for building C++ modules with CMake, Clang, and import std;.
This is a standalone, copy-pastable note aimed at real projects, not a language-lawyer overview. The emphasis is on the setup that tends to work in practice:
- put module interface units in
.cppmfiles - register them with CMake as a
CXX_MODULESfile set - build them into one library target
- let executables, tests, and bindings import from that target
- isolate third-party header-heavy code behind one vendor wrapper module
You need all of the following:
- a compiler with usable C++ modules support
- a standard library that supports
import std; - a CMake version with native module support
- a build toolchain that can scan module dependencies
In practice, this usually means:
- Clang
- Ninja
clang-scan-deps- a recent CMake
import std; is still the least portable part. A project can use C++ modules successfully even if import std; is unavailable, but then you will likely keep using #include <...> for the standard library.
cmake_minimum_required(VERSION 4.3.1)
# This UUID is required by current CMake support for `import std;`.
# Check your CMake/compiler docs in case it changes.
set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "451f2fe2-a8a2-47c3-bc32-94786d8fc91b")
project(myproj LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 26)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_MODULE_STD ON)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
file(GLOB_RECURSE MYPROJ_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/modules/*.cppm")
add_library(myproj_modules STATIC)
target_sources(myproj_modules PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES
BASE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/modules"
FILES ${MYPROJ_MODULES}
)
add_executable(mytool tools/mytool.cpp)
target_link_libraries(mytool PRIVATE myproj_modules)Core idea:
- CMake discovers the
.cppmfiles FILE_SET ... TYPE CXX_MODULEStells CMake these are module interface units- downstream targets do not compile the modules again
- downstream targets just link the library and
importthe modules
myproj/
CMakeLists.txt
modules/
myproj/
vendor.cppm
api.cppm
core/
graph.cppm
math.cppm
tools/
mytool.cpp
tests/
TestGraph.cpp
Recommended naming rule:
- file path should match module name
Examples:
modules/myproj/api.cppm->export module myproj.api;modules/myproj/core/graph.cppm->export module myproj.core.graph;
This keeps the project searchable and predictable.
Most project modules can use this shape:
export module myproj.core.graph;
import std;
import myproj.vendor;
import myproj.core.math;
namespace myproj {
struct Node {
int id{};
};
int helper(Node n) {
return n.id + 1;
}
export int next_id(Node n) {
return helper(n);
}
} // namespace myprojPattern to remember:
export module ...;declares the moduleimport std;pulls in the standard library as a moduleimport myproj.*;pulls in other project modules- declarations or definitions are exported only when marked
export
Private helpers can live in the same .cppm file. They are visible inside the module unit, but not to importers.
Regular .cpp files can import project modules directly. They do not need to be module units themselves.
import std;
import myproj.api;
int main() {
std::println("{}", myproj::version_string());
}This applies equally to:
- executables
- tests
- pybind11 binding TUs
- benchmarks
- one-off tools
That is often the simplest production setup: only library code is modularized, while top-level entry points remain ordinary .cpp files.
Use a global module fragment when you must do textual inclusion before the module declaration.
Typical reasons:
- third-party headers are not modular
- macros must be visible during inclusion
- header include order matters
- you need
extern "C"declarations - you need ABI glue, inline assembly, pragmas, or platform-specific declarations
Pattern:
module;
#include <third_party.hpp>
#include <legacy_c_api.h>
export module myproj.vendor;
export namespace vendor_ns {
using ::ThirdPartyType;
using ::third_party_function;
}The module; line begins the global module fragment. Everything before export module ...; behaves like ordinary textual compilation.
This is the right place for awkward includes and compatibility hacks.
One of the most useful real-world patterns is to create exactly one wrapper module for large third-party dependencies.
Example:
module;
#include <Eigen/Eigen>
#include <nlohmann/json.hpp>
#include <some_sdk/api.h>
export module myproj.vendor;
export namespace Eigen {
using Eigen::MatrixXd;
using Eigen::Vector3d;
using Eigen::Quaterniond;
}
export namespace nlohmann {
using nlohmann::json;
using nlohmann::ordered_json;
}
export namespace myproj::vendor::sdk {
using ::sdk_context;
using ::sdk_run;
}Why this pattern works well:
- it isolates header-heavy dependencies in one place
- it avoids spraying fragile
#includeorder across the codebase - project modules can simply
import myproj.vendor; - it gives you a curated exported surface instead of re-exporting everything blindly
This is often better than trying to turn every third-party library into its own clean module story.
It is valid for a normal header to import a module:
#pragma once
import myproj.vendor;
namespace myproj {
inline nlohmann::json make_json() {
return {{"ok", true}};
}
}This can be convenient, but remember:
- the file is still a normal header, so it is textually included
- keep such headers small
- avoid making a deeply nested include stack depend on large imported modules unless it genuinely helps
In many codebases, headers that import modules are best used sparingly.
Export only the public API.
Good candidates:
- public types
- public functions
- public constants
- concepts intended for external use
Usually keep private:
- helper functions
- implementation detail aliases
- internal glue code
- one-off local utilities
If everything in a module is marked export, your module boundary is probably too loose.
A simple and effective build model is:
- one library target owns all module interface units
- executables/tests/bindings link that library
- entry points remain ordinary
.cppfiles
This avoids a lot of complexity around cross-target module ownership.
You can split modules across multiple targets later, but it is usually better to start with one target unless there is a strong reason not to.
These conventions tend to keep modular code maintainable:
- keep one named module per
.cppm - keep module names stable and path-aligned
- use
import std;consistently if your toolchain supports it - put third-party includes in one wrapper module when possible
- avoid mixing many textual includes into ordinary module units
- keep tests and binaries as plain
.cppimporters
They do not. Third-party libraries may still require textual inclusion, macro setup, or strict include order. Use a global module fragment or a vendor wrapper module.
This recreates the old header mess inside your module graph. Centralize them when possible.
Support varies by compiler, standard library, and CMake version. Treat it as a toolchain feature, not a language guarantee that always works out of the box.
Start with one module-owning library target. Cross-target module boundaries can be introduced later if the build graph really needs them.
Only export the API you want importers to depend on. Keep helpers unexported.
They do not. A mixed setup is normal:
- module interface units for library code
- ordinary
.cppfiles for apps, tests, and bindings
export module myproj.feature;
import std;
import myproj.vendor;
namespace myproj {
struct State {
int value{};
};
export int run(const State& s) {
return s.value;
}
} // namespace myprojmodule;
#include <third_party.hpp>
#include <another_lib.hpp>
export module myproj.vendor;
export namespace third_party {
using ::SomeType;
using ::do_work;
}import std;
import myproj.feature;
int main() {
myproj::State s{42};
std::println("{}", myproj::run(s));
}- choose a compiler and CMake version with working module support
- enable
CMAKE_CXX_MODULE_STDandCMAKE_CXX_SCAN_FOR_MODULES - register
.cppmfiles withFILE_SET ... TYPE CXX_MODULES - build them into one library target
- import them from regular
.cppfiles - isolate ugly third-party includes in one wrapper module
- use the global module fragment when textual inclusion must happen before the module declaration
If you want one default answer for a production codebase, use this:
- Put all project modules in
modules/. - Name them by path.
- Build them into one
myproj_moduleslibrary target. - Keep apps/tests/bindings as normal
.cppfiles. - Create one
myproj.vendorwrapper module for third-party headers. - Use
module;only when you truly need textual inclusion beforeexport module.
That setup is simple, scalable, and much easier to maintain than an over-engineered module graph.