C++20 Modules Overview
C++20 modules are the most significant change to the C++ compilation model since the language's inception. They replace the decades-old textual inclusion mechanism (#include) with a proper module system that provides encapsulation, faster compilation, and cleaner dependency management.
Module vs Header: What Changes
| Aspect | Headers (#include) | Modules (import) |
|---|---|---|
| Mechanism | Textual copy-paste | Binary interface import |
| Parse frequency | Every TU that includes | Compiled once, imported many |
| Macro leakage | All macros leak across TUs | Macros don't cross module boundaries |
| Include order | Order-dependent (fragile) | Order-independent |
| Symbol visibility | Everything is visible | Only export-ed symbols visible |
| Build parallelism | High (independent TUs) | Constrained by DAG (module deps) |
| ODR violations | Common, hard to detect | Eliminated by design |
// Traditional header approach
// math_utils.h
#pragma once
#include <cmath>
#include <vector>
namespace math {
double mean(const std::vector<double>& data);
double stddev(const std::vector<double>& data);
}
// C++20 module approach
// math_utils.cppm (module interface unit)
export module math_utils;
import <cmath>;
import <vector>;
export namespace math {
double mean(const std::vector<double>& data);
double stddev(const std::vector<double>& data);
}
flowchart TD
APP[app.cpp
import mylib;] --> MYLIB[module mylib
mylib.cppm]
APP --> UTILS[module utils
utils.cppm]
MYLIB --> BASE[module base
base.cppm]
UTILS --> BASE
MYLIB --> STD[import std;]
BASE --> STD
UTILS --> STD
CMake Module Support
CMake added experimental C++20 module support in version 3.25 and stabilized it in 3.28. The support is documented in cmake-cxxmodules(7).
Minimum Requirements
# Minimum versions for C++20 module support:
cmake_minimum_required(VERSION 3.28) # Stable module support
project(ModulesDemo LANGUAGES CXX)
# REQUIRED: C++20 standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # Recommended for portability
CXX_SCAN_FOR_MODULES
CMake must scan module source files to determine the dependency DAG before compilation. This is controlled by the CXX_SCAN_FOR_MODULES target property:
# Enabled by default for targets with C++20 sources in CMake 3.28+
# You can explicitly control it:
set_target_properties(mylib PROPERTIES
CXX_SCAN_FOR_MODULES ON
)
# Or disable scanning for a target that doesn't use modules
set_target_properties(legacy_lib PROPERTIES
CXX_SCAN_FOR_MODULES OFF
)
Declaring Module Sources
Module interface units are declared using target_sources() with the FILE_SET syntax:
FILE_SET CXX_MODULES
add_library(mymath)
# Declare module interface units via FILE_SET
target_sources(mymath
PUBLIC
FILE_SET CXX_MODULES
FILES
src/math_utils.cppm # Module interface unit
src/statistics.cppm # Another module interface
)
# Regular implementation files (not modules)
target_sources(mymath
PRIVATE
src/math_utils_impl.cpp # Module implementation unit
src/statistics_impl.cpp
)
// src/math_utils.cppm — Module Interface Unit
export module math_utils;
import <vector>;
import <numeric>;
import <cmath>;
export namespace math {
double mean(const std::vector<double>& data) {
if (data.empty()) return 0.0;
return std::accumulate(data.begin(), data.end(), 0.0) / data.size();
}
double stddev(const std::vector<double>& data) {
if (data.size() < 2) return 0.0;
double m = mean(data);
double sum_sq = 0.0;
for (double x : data) {
sum_sq += (x - m) * (x - m);
}
return std::sqrt(sum_sq / (data.size() - 1));
}
} // namespace math
Try It: Build Your First C++20 Module
Create a project with a module interface unit (greeter.cppm) that exports a greet(std::string_view name) function. Declare it with FILE_SET CXX_MODULES in CMake. Build with Ninja and CMake 3.28+. Observe the .pcm (Clang) or .ifc (MSVC) files generated in the build directory.
Module Partitions
Module partitions allow splitting a large module into multiple files while presenting a single module interface to consumers:
// math_utils-types.cppm — Partition interface
export module math_utils:types;
export struct Statistics {
double mean;
double stddev;
double min;
double max;
size_t count;
};
export enum class Method {
Population,
Sample
};
// math_utils-compute.cppm — Partition interface
export module math_utils:compute;
import :types; // Import sibling partition
import <vector>;
export Statistics compute_stats(
const std::vector<double>& data,
Method method = Method::Sample
);
// math_utils.cppm — Primary module interface (re-exports partitions)
export module math_utils;
export import :types; // Re-export the types partition
export import :compute; // Re-export the compute partition
# CMakeLists.txt — declaring partitions
add_library(mymath)
target_sources(mymath
PUBLIC
FILE_SET CXX_MODULES
FILES
src/math_utils.cppm # Primary interface
src/math_utils-types.cppm # Partition: types
src/math_utils-compute.cppm # Partition: compute
)
# Implementation units for partitions
target_sources(mymath PRIVATE
src/math_utils-compute_impl.cpp
)
Compiler Support Matrix
C++20 module support varies significantly across compilers. Here's the current state (as of 2026):
| Compiler | Version | Module Status | Extension | Key Flags |
|---|---|---|---|---|
| MSVC | 19.34+ (VS 2022 17.4+) | Production-ready | .ixx | /std:c++20 (auto-enabled) |
| GCC | 14+ | Usable (improving) | .cppm | -std=c++20 -fmodules-ts |
| Clang | 16+ | Good support | .cppm | -std=c++20 (auto-scans) |
# Compiler-specific considerations
if(MSVC)
# MSVC uses .ixx extension by convention
# Module support enabled automatically with /std:c++20
# Generates .ifc (Interface File Cache) files
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# GCC uses .cppm extension
# Generates .gcm files in gcm.cache/
# May need: -fmodules-ts flag (added by CMake automatically)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# Clang uses .cppm extension
# Generates .pcm (Precompiled Module) files
# Excellent scanning support with Ninja
endif()
Generator Requirements
Module dependency scanning requires the build system to understand inter-file dependencies before compilation. This imposes constraints on which generators work:
dyndep) that modules require. Visual Studio generator works for MSVC only.
# REQUIRED: Use Ninja generator for module projects
cmake -S . -B build -G Ninja
# Or specify in CMakePresets.json
{
"configurePresets": [{
"name": "modules",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "20"
}
}]
}
# Ninja's dyndep feature enables:
# 1. Scan source files for module declarations/imports
# 2. Build dependency DAG dynamically
# 3. Compile modules in correct topological order
# 4. Maximize parallelism within constraints
flowchart LR
SCAN[Dependency Scan
P1689 format] --> DAG[Build DAG
Topological sort]
DAG --> COMPILE_BASE[Compile base.cppm
→ base.pcm]
COMPILE_BASE --> COMPILE_UTILS[Compile utils.cppm
→ utils.pcm]
COMPILE_BASE --> COMPILE_MYLIB[Compile mylib.cppm
→ mylib.pcm]
COMPILE_UTILS --> COMPILE_APP[Compile app.cpp
imports utils, mylib]
COMPILE_MYLIB --> COMPILE_APP
COMPILE_APP --> LINK[Link
→ executable]
Consuming Modules
When a target declares module sources via FILE_SET CXX_MODULES with PUBLIC visibility, dependent targets can import those modules:
# Library with modules
add_library(mathlib)
target_sources(mathlib
PUBLIC
FILE_SET CXX_MODULES
FILES src/math_utils.cppm
)
# Application that imports the module
add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE mathlib)
# → main.cpp can now use: import math_utils;
// main.cpp — consuming the module
import math_utils; // Import our library's module
import <iostream>;
import <vector>;
int main() {
std::vector<double> data = {1.0, 2.0, 3.0, 4.0, 5.0};
std::cout << "Mean: " << math::mean(data) << "\n";
std::cout << "StdDev: " << math::stddev(data) << "\n";
return 0;
}
PUBLIC/PRIVATE/INTERFACE semantics. A PUBLIC FILE_SET CXX_MODULES makes the module importable by consumers. PRIVATE module file sets are internal implementation modules not visible to dependents.
Mixing Headers and Modules
Most projects will need to gradually migrate from headers to modules. CMake supports this hybrid approach:
# Library with both headers (legacy) and modules (new)
add_library(mylib)
# Traditional headers still work
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# New module interface (alongside headers)
target_sources(mylib
PUBLIC
FILE_SET CXX_MODULES
FILES
src/mylib.cppm # Module wrapping existing functionality
PRIVATE
src/legacy_impl.cpp # Can use #include internally
src/module_impl.cpp # Module implementation
)
// src/mylib.cppm — Module that wraps existing header-based code
export module mylib;
// You can #include headers INSIDE a module's global fragment
module;
#include "internal_header.h" // Private to this module
export module mylib;
// Re-export functionality as module API
export {
using ::internal::Widget;
using ::internal::process;
}
// Header units — import headers as if they were modules
// (compiler must support this)
import <string>; // Standard library header unit
import <vector>; // More efficient than #include in module context
Try It: Wrap a Header Library as a Module
Take an existing header-only library (or create a simple one with 2-3 functions). Create a module interface unit that includes the header in its global module fragment and exports the public API. Verify that consumers can both #include the header (legacy) and import the module (modern) from the same library target.
Module Interface vs Implementation
C++20 modules separate interface from implementation — similar to header/source pairs but with proper language semantics:
// math_utils.cppm — Interface unit (compiled to .pcm/.ifc)
export module math_utils;
import <vector>;
// Only declarations here (like a header, but better)
export namespace math {
double mean(const std::vector<double>& data);
double stddev(const std::vector<double>& data);
double median(std::vector<double> data); // Takes by value intentionally
}
// math_utils_impl.cpp — Implementation unit
module math_utils; // No 'export' keyword — this is an implementation unit
import <algorithm>;
import <numeric>;
import <cmath>;
namespace math {
double mean(const std::vector<double>& data) {
if (data.empty()) return 0.0;
return std::accumulate(data.begin(), data.end(), 0.0)
/ static_cast<double>(data.size());
}
double stddev(const std::vector<double>& data) {
if (data.size() < 2) return 0.0;
double m = mean(data);
double accum = 0.0;
for (double x : data) {
accum += (x - m) * (x - m);
}
return std::sqrt(accum / (data.size() - 1));
}
double median(std::vector<double> data) {
std::sort(data.begin(), data.end());
size_t n = data.size();
if (n % 2 == 0)
return (data[n/2 - 1] + data[n/2]) / 2.0;
return data[n/2];
}
} // namespace math
File extension conventions by compiler:
| File Type | MSVC | GCC/Clang | Purpose |
|---|---|---|---|
| Module interface | .ixx | .cppm | Contains export module declaration |
| Module implementation | .cpp | .cpp | Contains module name; (no export) |
| Partition interface | .ixx | .cppm | Contains export module name:part; |
| Internal partition | .cpp | .cpp | Contains module name:part; |
Current Limitations and Workarounds
While C++20 modules in CMake have matured significantly, some limitations remain:
- Ninja required — Makefile generators do not support
dyndep - No install() support for modules — Installing module interfaces for consumption by other projects is still being standardized
- Limited cross-compilation — Module BMI (Binary Module Interface) files are compiler-version-specific
- IDE support varies — IntelliSense/clangd may have gaps with complex module hierarchies
- Header units inconsistent —
import <header>;support varies across compilers
# Workaround: Conditional module support
# Allow projects to build with or without modules
option(USE_CXX_MODULES "Enable C++20 modules (requires CMake 3.28+, Ninja)" OFF)
if(USE_CXX_MODULES)
if(CMAKE_VERSION VERSION_LESS "3.28")
message(FATAL_ERROR "C++20 modules require CMake 3.28+")
endif()
if(NOT CMAKE_GENERATOR MATCHES "Ninja")
message(FATAL_ERROR "C++20 modules require Ninja generator")
endif()
add_library(mylib)
target_sources(mylib
PUBLIC FILE_SET CXX_MODULES FILES src/mylib.cppm
PRIVATE src/mylib_impl.cpp
)
else()
# Fallback: traditional header-based build
add_library(mylib src/mylib.cpp)
target_include_directories(mylib PUBLIC include/)
endif()
# Version requirements summary
# CMake 3.25: Experimental module support (CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP)
# CMake 3.26: Improved scanning, better error messages
# CMake 3.27: FILE_SET CXX_MODULES stabilizing
# CMake 3.28: STABLE — no experimental flags needed
# CMake 3.29+: install() for modules (in progress)
Try It: Complete Module-Based Project
Create a project with two libraries (core and utils) where utils imports core's module. Add an application that imports both. Use module partitions in the core library. Build with Ninja and verify the correct compilation order. Try building with Make to confirm it fails with a helpful error.