Skip to content

Instantly share code, notes, and snippets.

@yushijinhun
Created April 21, 2026 05:52
Show Gist options
  • Select an option

  • Save yushijinhun/d06c846f2687e25077d8e8f8b9de19bb to your computer and use it in GitHub Desktop.

Select an option

Save yushijinhun/d06c846f2687e25077d8e8f8b9de19bb to your computer and use it in GitHub Desktop.
C++26 Modules Cheat Sheet

C++26 Modules Cheat Sheet

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 .cppm files
  • register them with CMake as a CXX_MODULES file 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

1. Toolchain assumptions

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.

2. Minimal CMake setup

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 .cppm files
  • FILE_SET ... TYPE CXX_MODULES tells CMake these are module interface units
  • downstream targets do not compile the modules again
  • downstream targets just link the library and import the modules

3. Recommended project shape

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.

4. Normal module interface unit

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 myproj

Pattern to remember:

  • export module ...; declares the module
  • import std; pulls in the standard library as a module
  • import 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.

5. Consumer translation units

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.

6. Global module fragment

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.

7. Vendor wrapper module

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 #include order 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.

8. Headers can import modules too

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.

9. What to export

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.

10. One library target for all modules

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 .cpp files

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.

11. Practical conventions

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 .cpp importers

12. Common pitfalls

Pitfall: assuming modules remove all include problems

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.

Pitfall: scattering third-party includes across many modules

This recreates the old header mess inside your module graph. Centralize them when possible.

Pitfall: expecting import std; to work everywhere

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.

Pitfall: splitting too early into many module targets

Start with one module-owning library target. Cross-target module boundaries can be introduced later if the build graph really needs them.

Pitfall: exporting too much

Only export the API you want importers to depend on. Keep helpers unexported.

Pitfall: thinking module units must replace every .cpp

They do not. A mixed setup is normal:

  • module interface units for library code
  • ordinary .cpp files for apps, tests, and bindings

13. Copy-paste templates

Template: project module

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 myproj

Template: vendor module

module;

#include <third_party.hpp>
#include <another_lib.hpp>

export module myproj.vendor;

export namespace third_party {
using ::SomeType;
using ::do_work;
}

Template: consumer TU

import std;
import myproj.feature;

int main() {
    myproj::State s{42};
    std::println("{}", myproj::run(s));
}

14. Minimal checklist

  • choose a compiler and CMake version with working module support
  • enable CMAKE_CXX_MODULE_STD and CMAKE_CXX_SCAN_FOR_MODULES
  • register .cppm files with FILE_SET ... TYPE CXX_MODULES
  • build them into one library target
  • import them from regular .cpp files
  • isolate ugly third-party includes in one wrapper module
  • use the global module fragment when textual inclusion must happen before the module declaration

15. Recommended default architecture

If you want one default answer for a production codebase, use this:

  1. Put all project modules in modules/.
  2. Name them by path.
  3. Build them into one myproj_modules library target.
  4. Keep apps/tests/bindings as normal .cpp files.
  5. Create one myproj.vendor wrapper module for third-party headers.
  6. Use module; only when you truly need textual inclusion before export module.

That setup is simple, scalable, and much easier to maintain than an over-engineered module graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment