Five Years In, C++20 Modules Are Still a Library Developer's Obstacle Course
Source: isocpp
C++20 modules have been in the standard for over five years. The pitch was compelling: faster builds, genuine encapsulation, no more macro leakage, a step toward a sane include model. The reality, as anyone who has tried to actually ship module support in a public library has discovered, is that the distance between the standard and a working ecosystem is still significant.
Rubén Pérez Hidalgo, the maintainer of Boost.MySQL, gave a talk at isocpp.org tracing the practical journey of adding import boost.mysql to a real library. Boost.MySQL shipped module support in its 1.88 release in April 2025, which sounds like a success story. It is, but it took an unusual amount of effort for something the standard had supposedly resolved half a decade ago.
What Modules Were Supposed to Solve
The core problem with headers is not just build speed, though that is the most visible symptom. The deeper issue is that #include is textual substitution. Every translation unit that includes a header re-parses and re-processes everything in that header, including all of its transitive includes. On large codebases this produces enormous redundancy, and it makes the compilation model fragile in ways that modules are designed to eliminate.
With modules, a module interface unit is compiled once to a Binary Module Interface (BMI), and consumers import that precompiled artifact instead of re-parsing source. The compiler also enforces proper encapsulation: names that are not explicitly exported are invisible to importers, which eliminates a whole class of accidental dependency on internal symbols. Macros, famously, cannot cross module boundaries at all.
A module interface unit looks like this:
export module mylib;
export namespace mylib {
class Connection { /* ... */ };
void connect(Connection&);
}
And a consumer just writes:
import mylib;
mylib::Connection conn;
That simplicity is real. The problem is everything between writing the interface and getting that import to work for downstream users.
The Macro Problem Is Not Fully Solved
Macros cannot be exported from modules. This is intentional, because modules are supposed to eliminate macro-based APIs. But C++ libraries, including most of Boost, rely heavily on macros for configuration, platform detection, and feature flags. A library that exposes a macro-based API cannot just migrate to modules and drop the macros without breaking users.
The answer the standard provides is the global module fragment: a section before the module declaration where #include directives are still permitted, and where macros defined by those includes remain available within the module unit.
module; // global module fragment begins
#include <system_header.h> // macros from here are usable below
#include "config_macros.h"
export module mylib; // module preamble begins here
export namespace mylib { /* ... */ }
This works for consuming macros from legacy headers inside your module. What it does not solve is users who depend on macros your library exports. Those users must still include the old headers. Modules and headers have to coexist in a dual-interface arrangement, which is exactly what Boost.MySQL and similar libraries must maintain: both the traditional header interface and a new module interface, kept in sync, tested separately.
Maintaining two interfaces is not free. It adds surface area, complicates CI, and creates divergence risk. Barry Revzin’s detailed breakdown of the ecosystem problem covers this and other friction points that make library-level adoption slow.
Build System Support Took Years to Arrive
Modules fundamentally change the build dependency model. With headers, a build system can scan source files for includes before compilation begins, since the dependency graph is static. With modules, a translation unit that imports another module must wait for that module’s BMI to exist before it can be compiled. The dependency graph must be resolved at build time, not before it.
This is a real problem for build systems that were not designed around dynamic dependencies. CMake added experimental module support in 3.28 in December 2023, and improved it in 3.30 in mid-2024. The CMake documentation currently still says the implementation is incomplete and recommends reading the known issues page before using modules in production. That is not a strong endorsement.
The build system has to scan module sources to discover their dependencies before scheduling compilation, a process called dependency scanning. All three major compilers have different output formats for this scan data, and the standardization of a common format (P1689) took until 2022 and only became widely supported over the following years.
For a library like Boost.MySQL, which needs to support multiple build systems including CMake and B2, this means implementing and maintaining module support for each build system separately, each with its own quirks and maturity level.
BMI Files Are Not Portable
A Binary Module Interface is compiler-specific and version-specific. A BMI produced by Clang 18 is not readable by GCC 14, and may not even be readable by Clang 19. This means a library cannot distribute prebuilt BMIs the way it might distribute prebuilt static libraries. Every user must compile the module themselves from source.
This has real consequences for package managers and distribution. Vcpkg, Conan, and similar tools that distribute prebuilt binaries have to handle modules differently. Clang’s module documentation notes that ABI compatibility between modules even within the same compiler version is not guaranteed across all configurations.
The lack of a portable BMI format means the build time savings that modules promise are contingent on users building from source, which most package manager consumers do not prefer. The compilation cost is pushed to the user’s first build rather than eliminated.
Compiler Support Is Uneven
All three major compilers have C++20 module support, but the level of completeness and stability varies across versions.
Clang has the most mature implementation, with support for named modules, header units, and module partitions. GCC added module support in GCC 11 but the implementation has historically had more rough edges. MSVC has had support since Visual Studio 2019 and has iterated significantly, including C++23 import std support in VS 2022 17.13.
For a Boost library that needs to compile on all three compilers across several versions, this creates a matrix of compiler-specific workarounds. Features or syntax that work on one compiler may silently misbehave or refuse to compile on another. Module partitions, in particular, have had varying support quality across compiler versions.
Module Partitions for Large Libraries
A library like Boost.MySQL is not a small codebase. Putting the entire public API into a single module interface unit would create a massive file and lose the organizational benefits of splitting code across files. Module partitions solve this by allowing a module to be composed from multiple units:
// boost.mysql:connection (partition)
export module boost.mysql:connection;
export namespace boost::mysql {
class connection { /* ... */ };
}
// boost.mysql (primary interface, aggregates partitions)
export module boost.mysql;
export import :connection;
export import :resultset;
export import :statement;
This structure lets the library maintain internal file organization while presenting a single import boost.mysql to users. But it also means the build system needs to correctly understand partition dependencies, and the compiler has to handle the aggregation correctly. Any bug in partition handling in any compiler on the support matrix can block a release.
The Actual Cost
When Boost.MySQL shipped module support in 1.88, it required solving all of the above simultaneously: a dual header/module interface, build system support in both CMake and B2, workarounds for compiler differences, handling the macro boundary through the global module fragment, and extensive testing to ensure both interfaces remain in sync across platforms.
This is the gap that the isocpp.org talk documents. The standard specified modules, but the standard does not ship build systems, compiler implementations, package managers, or documentation tools. All of those had to catch up independently, on different timelines, with different priorities.
For a library with fewer resources than Boost.MySQL, or without a maintainer willing to invest this kind of sustained effort, the calculation is straightforward: wait. The ecosystem is functional enough to use modules in application code, especially if you control your full build pipeline, but shipping them as a public library surface remains a significant undertaking.
Rust’s module system is a useful contrast. The mod and use system shipped with the language, and the centralized toolchain meant Cargo, rustc, and crates.io all understood the dependency model from day one. No parallel include system to maintain, no BMI portability problem, no build system lag. The C++ situation reflects the decentralized nature of the ecosystem: the standard body, compiler vendors, build system maintainers, and package managers all move independently. That independence has real benefits for the ecosystem’s diversity and stability, but it makes coordinated migrations slow.
Modules will get there. Boost.MySQL proving it works in a real library with real users is valuable evidence. The path just requires considerably more patience than the five-year-old standard date suggests.