· 7 min read ·

Five Years In, C++20 Modules Are Still Waiting for the Ecosystem to Catch Up

Source: isocpp

C++20 was finalized in late 2020, and modules were its most structurally significant addition. Not a library feature or a syntax convenience but a fundamental change to how translation units are organized, how names are exported, and how compilers consume dependencies. Five years later, Rubén Pérez Hidalgo gave a talk at Meeting C++ 2024 about adding module support to Boost.MySQL. The fact that it took an experienced library maintainer enough effort to warrant a conference presentation says something honest about where the ecosystem actually stands.

What Modules Were Supposed to Fix

The traditional C++ header model has accumulated decades of friction. Every translation unit that includes a header reprocesses it from scratch, preprocessor macros leak across boundaries, include order can change behavior, and build times scale poorly as projects grow. The #include model was inherited from C and has never been revisited in any fundamental way.

Modules address this by replacing the textual inclusion model with a semantic one. A module interface unit declares what it exports using the export keyword. Consumers use import instead of #include. The compiler serializes the module’s interface into a Binary Module Interface (BMI) file, and downstream consumers read that instead of reprocessing source text.

A minimal module interface looks like this:

// mylib.cppm
export module mylib;

export namespace mylib {
    struct Connection {
        void connect(const char* host, int port);
    };
}

And the consumer:

import mylib;

int main() {
    mylib::Connection conn;
    conn.connect("localhost", 3306);
}

The interface is clean. The implementation separates cleanly from the interface through module implementation units. Module partitions let large modules split across files without exposing internal structure to consumers. On paper, this solves a real problem.

The Build System Problem

In practice, modules introduce a dependency ordering problem that traditional build systems were not designed to handle. With headers, all files can be compiled in any order because the preprocessor resolves includes at parse time. With modules, before compiling a file that says import foo, the compiler needs the BMI for foo. Build systems must now do dependency scanning before they can schedule compilation jobs.

This is not a minor change. It requires build systems to understand C++ source structure, not just file modification times and explicit dependency lists. CMake added experimental module support in version 3.28, released in December 2023, three years after the standard was finalized. Support is limited to Ninja and Visual Studio generators. GCC support requires version 14 or newer. MSVC support requires toolset 14.34 or newer. Clang requires version 16 or newer.

Those requirements narrow the field considerably. Many production environments run older toolchains. CI pipelines on older Ubuntu LTS images will not have GCC 14. Windows developers who haven’t updated Visual Studio recently are out of luck. Boost.MySQL’s module documentation lists the requirements explicitly: CMake 3.28 or newer, Ninja 1.11 or newer (or Visual Studio 2022), and a compliant C++20 compiler. That’s a fairly specific combination.

What Boost.MySQL Actually Had to Do

Boost.MySQL is a header-only library, which represents the dominant distribution model for Boost components and for C++ libraries generally. Header-only libraries are easy to consume: drop the headers in your include path and compile. Modules are not header-only; they produce compiled artifacts that must be linked.

This creates a practical split in how the library must be offered. The module variant of Boost.MySQL is an opt-in feature that compiles a module object file and links it into the final executable. Users who want the module interface get a compiled artifact to manage. Users who want the traditional experience keep using headers. Maintaining both paths simultaneously means more test matrix surface, more CMake logic, and careful attention to which features are available in which mode.

The library also had to navigate the macro problem. Macros do not cross module boundaries. Any configuration or platform-detection logic that uses macros internally works fine, but macros cannot be exported to consumers. Libraries that have historically relied on users defining macros to configure behavior need alternative APIs for the module interface.

There is also the question of what to do about the transition period. A consumer who imports your module may also be pulling in a dependency that still uses headers. Mixing import and #include in the same translation unit is allowed but comes with rules about ordering and the global module fragment. The global module fragment (module; at the top of a file, before the export module declaration) exists precisely to accommodate legacy header includes:

module;  // global module fragment
#include <legacy_header.h>  // allowed here

export module mymodule;
// actual module content

This is a real concession. It acknowledges that the ecosystem will not flip over at once and that modules need to coexist with the header world for the foreseeable future.

The BMI Compatibility Gap

One constraint that does not get enough attention is that BMI files are not portable between compilers, and sometimes not even between versions of the same compiler. They are internal binary formats that encode the compiler’s representation of the module interface. This means you cannot distribute precompiled BMIs in a package the way you might distribute precompiled object files or static libraries.

Package managers like vcpkg and Conan have to rebuild modules from source for each compiler configuration. Build performance comparisons between C++17 headers, C++20 modules, and modular headers show that modules do improve incremental build times in practice, but the first-build cost is at least comparable to precompiled headers, and the tooling to make that work is still maturing.

This stands in contrast to how Rust handles the situation. Rust’s crate system is built around compilation units from the ground up. The compiler produces .rlib files that are versioned and understood by the package manager. Cargo handles dependency ordering automatically because it was designed with this model. The Rust module system is not bolted onto a preprocessor-based heritage; it is the heritage. C++ modules are trying to retrofit a fundamentally different compilation model onto tooling that took decades to stabilize around the old one.

Where the Ecosystem Actually Is

Eric Niebler’s CppCon 2024 talk offered a frank assessment: six years in, the state of module support in tooling is not great. That is a considered judgment from someone deeply embedded in the C++ standardization community, not a complaint from a frustrated user.

The good news is that Boost.MySQL is not alone. As of early 2025, Boost.Asio, Boost.Beast, and Boost.MySQL all have C++20 module support. That matters because Asio is one of the most widely used networking libraries in C++, and its module support sets a pattern others can follow. The Boost ecosystem moving toward modules, even incrementally, is more significant than any single toolchain update.

For compiler support, the situation in 2026 is meaningfully better than it was in 2023. GCC 14 and 15 have progressively improved module implementation. Clang’s module support, while historically more advanced, has also matured. MSVC has been ahead of the other compilers on build system integration, partly because Microsoft controls both the compiler and Visual Studio and could coordinate the changes.

The standard library itself is now available as a module in some implementations. MSVC has supported import std; for some time. GCC added it in version 15. When import std; works reliably across all major compilers, that will be the clearest signal that modules are ready for mainstream use.

What This Means for Library Authors Right Now

If you maintain a C++ library and are thinking about adding module support, the honest answer is that it is worth doing but not yet worth making the primary interface. The constraints Boost.MySQL documents are real: you need a recent toolchain, a modern build system, and users who have both.

A pragmatic path is to offer modules as an opt-in alongside the existing header interface, with the two maintained in sync. Use the global module fragment to wrap third-party includes. Export a clean public interface and keep implementation details in non-exported module partitions. Document the toolchain requirements prominently.

Do not attempt to use header units as a migration shortcut. Header units (importing a header with import <header>) look like a gentle transition path, but their semantics are subtle, compiler support is inconsistent, and they do not give you the clean interface separation that named modules provide. They are an incomplete bridge.

The tooling will continue to improve. CMake’s module support is no longer marked experimental as of CMake 3.30. Package managers are adding better support for module-aware builds. The trajectory is clear even if the pace has been frustrating.

What Rubén Pérez Hidalgo’s work on Boost.MySQL demonstrates is that adding module support to a mature library is tractable, though it requires understanding which toolchain combinations actually work, structuring the library to coexist gracefully with the header model, and accepting that you are building infrastructure for a future state of the ecosystem rather than serving most of your current users. That is an uncomfortable position for a library maintainer, but it is the honest one.

Was this interesting?