From fc52022c516c4ebd48294691d6e109a69ab8d934 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 22 Dec 2024 12:31:06 +0100 Subject: [PATCH 01/19] Install CXX_MODULES --- include/BoostInstall.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/include/BoostInstall.cmake b/include/BoostInstall.cmake index 1127c6f..6fadf96 100644 --- a/include/BoostInstall.cmake +++ b/include/BoostInstall.cmake @@ -304,6 +304,7 @@ function(boost_install_target) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" PRIVATE_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + FILE_SET CXX_MODULES DESTINATION . ) export(TARGETS ${LIB} NAMESPACE Boost:: FILE export/${LIB}-targets.cmake) From 3d6703b85de1109e0d1d095e9a3587aaf3a09bc5 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 22 Dec 2024 13:04:10 +0100 Subject: [PATCH 02/19] Helper function for C++20 modules --- include/BoostRoot.cmake | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/include/BoostRoot.cmake b/include/BoostRoot.cmake index 98a19df..86e56d1 100644 --- a/include/BoostRoot.cmake +++ b/include/BoostRoot.cmake @@ -239,6 +239,29 @@ macro(__boost_add_header_only lib) endmacro() +# C++20 modules +option(BOOST_CXX20_MODULE "Build Boost as a collection of C++20 modules (unstable)" OFF) + +# Define a helper function to set options for targets that implement modules +if (BOOST_CXX20_MODULE) + function (boost_set_cxx20_module_settings TARGET_NAME) + # All modules use import std, which requires C++23, and this should propagate + target_compile_features(${TARGET_NAME} PUBLIC cxx_std_23) + + # Enable import std + set_target_properties(${TARGET_NAME} PROPERTIES CXX_MODULE_STD 1) + + # C++ code uses this macro to determine whether we're using modules or not. Should propagate + target_compile_definitions(${TARGET_NAME} PUBLIC BOOST_CXX20_MODULE) + + # Most libraries implement the module by including its own headers in the module purview. + # clang will complain if it sees an #include in the module purview. + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(${TARGET_NAME} PRIVATE -Wno-include-angled-in-module-purview) + endif() + endfunction() +endif() + # file(GLOB __boost_libraries RELATIVE "${BOOST_SUPERPROJECT_SOURCE_DIR}/libs" "${BOOST_SUPERPROJECT_SOURCE_DIR}/libs/*/CMakeLists.txt" "${BOOST_SUPERPROJECT_SOURCE_DIR}/libs/numeric/*/CMakeLists.txt") From ca5e12b6475e6753b3c3bd56d8feec3d87862101 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sun, 22 Dec 2024 18:14:21 +0100 Subject: [PATCH 03/19] Warning suppression for MSVC --- include/BoostRoot.cmake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/BoostRoot.cmake b/include/BoostRoot.cmake index 86e56d1..c1baef5 100644 --- a/include/BoostRoot.cmake +++ b/include/BoostRoot.cmake @@ -255,9 +255,11 @@ if (BOOST_CXX20_MODULE) target_compile_definitions(${TARGET_NAME} PUBLIC BOOST_CXX20_MODULE) # Most libraries implement the module by including its own headers in the module purview. - # clang will complain if it sees an #include in the module purview. + # Some compilers complain if they see an #include in the module purview. if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(${TARGET_NAME} PRIVATE -Wno-include-angled-in-module-purview) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options(${TARGET_NAME} PRIVATE /wd5244) endif() endfunction() endif() From 0f5e4aca53ab02399f43cc0b5462c17faef6d261 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Dec 2024 10:56:41 +0100 Subject: [PATCH 04/19] Install modules to datadir --- include/BoostInstall.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/BoostInstall.cmake b/include/BoostInstall.cmake index 6fadf96..23e3a46 100644 --- a/include/BoostInstall.cmake +++ b/include/BoostInstall.cmake @@ -304,7 +304,7 @@ function(boost_install_target) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" PRIVATE_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - FILE_SET CXX_MODULES DESTINATION . + FILE_SET CXX_MODULES DESTINATION "${CMAKE_INSTALL_DATADIR}" ) export(TARGETS ${LIB} NAMESPACE Boost:: FILE export/${LIB}-targets.cmake) From f412580c640c04175adee20429ea521355a9304b Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 30 Dec 2024 11:21:46 +0100 Subject: [PATCH 05/19] Rename config macro to BOOST_USE_MODULES --- include/BoostRoot.cmake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/BoostRoot.cmake b/include/BoostRoot.cmake index c1baef5..dc89d81 100644 --- a/include/BoostRoot.cmake +++ b/include/BoostRoot.cmake @@ -240,10 +240,10 @@ macro(__boost_add_header_only lib) endmacro() # C++20 modules -option(BOOST_CXX20_MODULE "Build Boost as a collection of C++20 modules (unstable)" OFF) +option(BOOST_USE_MODULES "Build Boost as a collection of C++20 modules (unstable)" OFF) # Define a helper function to set options for targets that implement modules -if (BOOST_CXX20_MODULE) +if (BOOST_USE_MODULES) function (boost_set_cxx20_module_settings TARGET_NAME) # All modules use import std, which requires C++23, and this should propagate target_compile_features(${TARGET_NAME} PUBLIC cxx_std_23) @@ -252,7 +252,7 @@ if (BOOST_CXX20_MODULE) set_target_properties(${TARGET_NAME} PROPERTIES CXX_MODULE_STD 1) # C++ code uses this macro to determine whether we're using modules or not. Should propagate - target_compile_definitions(${TARGET_NAME} PUBLIC BOOST_CXX20_MODULE) + target_compile_definitions(${TARGET_NAME} PUBLIC BOOST_USE_MODULES) # Most libraries implement the module by including its own headers in the module purview. # Some compilers complain if they see an #include in the module purview. From 17b3387ec882851933fb6dc0dd792c5416ae6bc1 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Fri, 10 Jan 2025 19:41:26 +0100 Subject: [PATCH 06/19] Remove common function for modules --- include/BoostRoot.cmake | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/include/BoostRoot.cmake b/include/BoostRoot.cmake index dc89d81..f6faff7 100644 --- a/include/BoostRoot.cmake +++ b/include/BoostRoot.cmake @@ -242,28 +242,6 @@ endmacro() # C++20 modules option(BOOST_USE_MODULES "Build Boost as a collection of C++20 modules (unstable)" OFF) -# Define a helper function to set options for targets that implement modules -if (BOOST_USE_MODULES) - function (boost_set_cxx20_module_settings TARGET_NAME) - # All modules use import std, which requires C++23, and this should propagate - target_compile_features(${TARGET_NAME} PUBLIC cxx_std_23) - - # Enable import std - set_target_properties(${TARGET_NAME} PROPERTIES CXX_MODULE_STD 1) - - # C++ code uses this macro to determine whether we're using modules or not. Should propagate - target_compile_definitions(${TARGET_NAME} PUBLIC BOOST_USE_MODULES) - - # Most libraries implement the module by including its own headers in the module purview. - # Some compilers complain if they see an #include in the module purview. - if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - target_compile_options(${TARGET_NAME} PRIVATE -Wno-include-angled-in-module-purview) - elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - target_compile_options(${TARGET_NAME} PRIVATE /wd5244) - endif() - endfunction() -endif() - # file(GLOB __boost_libraries RELATIVE "${BOOST_SUPERPROJECT_SOURCE_DIR}/libs" "${BOOST_SUPERPROJECT_SOURCE_DIR}/libs/*/CMakeLists.txt" "${BOOST_SUPERPROJECT_SOURCE_DIR}/libs/numeric/*/CMakeLists.txt") From 6d4c13c47d524f0f5717709fa1608342e4a5483c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 26 Feb 2026 21:52:27 +0100 Subject: [PATCH 07/19] Install modules --- include/BoostInstall.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/include/BoostInstall.cmake b/include/BoostInstall.cmake index ca0997c..2a03e61 100644 --- a/include/BoostInstall.cmake +++ b/include/BoostInstall.cmake @@ -344,6 +344,7 @@ function(boost_install_target) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" PRIVATE_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + FILE_SET CXX_MODULES DESTINATION "${CMAKE_INSTALL_DATADIR}" ) export(TARGETS ${LIB} NAMESPACE Boost:: FILE export/${LIB}-targets.cmake) From afbc1f867a3dca6745c80f6c03fcb606ed26f273 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 5 Mar 2026 17:02:18 +0100 Subject: [PATCH 08/19] Fix older cmake version --- include/BoostInstall.cmake | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/BoostInstall.cmake b/include/BoostInstall.cmake index 2a03e61..3e1b430 100644 --- a/include/BoostInstall.cmake +++ b/include/BoostInstall.cmake @@ -337,6 +337,12 @@ function(boost_install_target) string(APPEND CONFIG_INSTALL_DIR "-static") endif() + # C++20 modules supported since CMake 3.28 + set(__BOOST_INSTALL_FILE_SET_ARGS) + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28) + set(__BOOST_INSTALL_FILE_SET_ARGS FILE_SET CXX_MODULES DESTINATION "${CMAKE_INSTALL_DATADIR}") + endif() + install(TARGETS ${LIB} EXPORT ${LIB}-targets # explicit destination specification required for 3.13, 3.14 no longer needs it RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" @@ -344,7 +350,7 @@ function(boost_install_target) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" PRIVATE_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" - FILE_SET CXX_MODULES DESTINATION "${CMAKE_INSTALL_DATADIR}" + ${__BOOST_INSTALL_FILE_SET_ARGS} ) export(TARGETS ${LIB} NAMESPACE Boost:: FILE export/${LIB}-targets.cmake) From 399ade9225fe6b68e209d6bdc3470fdc98048a2c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 5 Mar 2026 19:50:10 +0100 Subject: [PATCH 09/19] Modules docs 1 --- modules.md | 350 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 modules.md diff --git a/modules.md b/modules.md new file mode 100644 index 0000000..996f7a4 --- /dev/null +++ b/modules.md @@ -0,0 +1,350 @@ +# C++20 modules in Boost + +Audience: users that want to consume Boost as C++20 modules. +Library authors that want to support C++20 modules in their libraries. + +Guidelines to add C++20 module support for existing Boost libraries. + + +## Goals + +* Code should be usable as a module ('import boost.xyz') and as headers. + Existing code should not break. +* Feature-completeness. When consuming a library as a module, the entire + library should be usable and work correctly. +* Focus on compile times. Consuming Boost as a module should be able to reduce + compile times as much as possible. +* Isolation. Importing a library should pour only bring in the public identifiers. +* Maintainability. Modular bindings add clutter to the code. We aim at getting + as less clutter as possible. +* Ease of use with CMake. Users need to be able to consume Boost with C++20 modules + with CMake with the same easiness as they do without modules. + No manual building of .ixx files should happen. + +Non-goals + +* Mixing including and importing Boost. Boost will still work regularly using + headers. If you encounter this need, just use headers. +* Supporting toolchains with partial modular support. + We're currently targeting the latest toolchain versions, and these still contain + bugs. +* Supporting toolchains with modules but no import std. Import std is crucial + for builds performance. We only support configurations that allow importing std. + +## User interface + +* To use Boost with modules, users need to build it with CMake, enabling support for it explicitly. + This is done by specifying the `-DBOOST_USE_MODULES=1` CMake option when building Boost. +* Users can consume this built version of Boost either by using `add_subdirectory`, + or by installing it and then using `find_package`. +* Each Boost library gets its own C++20 module. For example, Boost.Mp11 can be consumed with `import boost.mp11`. +* When consuming a Boost distribution that uses modules, all public Boost headers are translated into their + corresponding import, plus any documented macro definition. + This is done using the preprocessor and defining the `BOOST_USE_MODULES`, which happens automatically when using CMake. + We call this "compatibility headers". + +## Modularizing header-only libraries + +At this point in time, modularizing a library requires modularizing its dependencies. +This can probably be lifted, but has not been researched yet (and order gives better build-time performance results). + +The recommended approach is the "ABI breaking" style. The idea is marking all the library public entities with +a `BOOST_XYZ_MODULE_EXPORT` macro that expands to `export` when building with modules, then include +all the library public headers in the module purview. + +The rest of this section is a step-by-step guide on how to create the modular interface. + +### Primary interface unit + +Your library should contain a single module unit, and it should be a primary interface unit. +For an hypothetical Boost.Xyz, it should be named `boost_xyz.cppm` and placed under the +`modules/` folder. + +This is what it should contain in a first iteration (we will add things later): + +```cpp +// Global module fragment empty for now. We will add things later here +module; + +// Begins the module purview +export module boost.xyz; + +// Include any dependency that your library might have +import std; +import boost.core; + +// We will use these macros when we get to compatibility headers +#define BOOST_XYZ_INTERFACE_UNIT +#define BOOST_IN_MODULE_PURVIEW + +// Including headers with #include <> triggers warnings. +// If you're following this guide, they should be safe to ignore. +// This header disables them +#include + +// Now include your library. Add any other header that exports symbols +#include +``` + +We'll get more in-depth into why each statement is required later. +For now, have in mind that we're just including our entire library in the module purview. + +### CMake code + +Next, we need to add code to our `CMakeLists.txt` to that CMake +knows how to build our code. If you're following Boost best practice, +your code may look like: + +```cmake +cmake_minimum_required(VERSION 3.5...3.31) + +project(boost_xyz VERSION "${BOOST_SUPERPROJECT_VERSION}" LANGUAGES CXX) + +add_library(boost_xyz INTERFACE) +add_library(Boost::xyz ALIAS boost_xyz) + +target_include_directories(boost_xyz INTERFACE include) + +target_link_libraries(boost_xyz + INTERFACE + Boost::core +) +``` + +Module units are translation units, so our when using modules, +our library will no longer be an INTERFACE library. +A small binary will get generated, containing the module initializer only. +(TODO: can we make this a footnote?) This is a function that initializes +global variables. It should be a no-op in most header-only libraries. +This is what the CMake code looks like after the change: + +```cmake +if (BOOST_USE_MODULES) + add_library(boost_xyz STATIC) + target_sources(boost_xyz PUBLIC FILE_SET CXX_MODULES BASE_DIRS modules FILES modules/boost_xyz.cppm) + set(__scope PUBLIC) + + target_compile_features(boost_xyz PUBLIC cxx_std_23) # import std requires C++23 + set_target_properties(boost_xyz PROPERTIES CXX_MODULE_STD 1) # Enable import std + target_compile_definitions(boost_xyz PUBLIC BOOST_USE_MODULES) # Preprocessor macro + +else() + add_library(boost_xyz INTERFACE) + set(__scope INTERFACE) +endif() + +add_library(Boost::xyz ALIAS boost_xyz) +target_include_directories(boost_xyz ${__scope} include) +target_link_libraries(boost_xyz + ${__scope} + Boost::core +) +``` + +* We use a STATIC library (vs. not specifying a type and letting the user choose) + because the binary is expected to contain almost no code. + It should make initialization more efficient and simplify deployment. +* We use `__scope` because it's an error to use `PUBLIC` with an interface library. +* `modules/boost_xyz.cppm` will be installed alongside the produced binary. + This happens automatically. This is required because BMIs must be regenerated + by consumers. + + +### Marking exported entities + +By default, entities defined in the module purview are not exported, +and won't be visible to importers. We need to mark public names with the `export` keyword. +To maintain compatibility, we advise to define a macro like this: + +```cpp +// For example, in boost/xyz/detail/config.hpp +#ifdef BOOST_USE_MODULES +# define BOOST_XYZ_MODULE_EXPORT export +#else +# define BOOST_XYZ_MODULE_EXPORT +#endif +``` + +We should now stick `BOOST_XYZ_MODULE_EXPORT` in front of every entity exported by the library. + +For templates, only the primary template should be exported. +Specializations should not. This means that if you're specializing +a template from the standard library, you don't need to export it. +It will be made available to importers by the compiler automatically. + +### Disabling includes for dependencies + +As you might know, anything included in the module purview +will get attached to our `boost.xyz` named module. +Only the names that our library exports should be attached there, +and not the names defined by our dependencies. +This is why compilers warn when using `#include <>` in the module +purview. + +While you can manually add `#ifdef` blocks for every dependency, +this is usually error-prone. For the standard library, Boost.Config +offers "compatibility headers" that will become a no-op when +building with modules. + +For instance, if a header contains this: + +```cpp +// ... +#include +#include +// ... +``` + +You can write: + +```cpp +// ... +#include +#include +// ... +``` + +This will expand to nothing in our purview, and is equivalent to +the former when `BOOST_USE_MODULES` is not defined. + +Some standard library headers also export macros. Boost.Config +compatibility headers don't export any macros. If you need the macros, +you need to include the original header in the global module fragment: + +```cpp +// +// header.hpp +// +#include + +// HUGE_VAL is a macro defined in +inline double f() { return HUGE_VAL; } + +// +// boost_xyz.cppm +// +module; + +#include // make HUGE_VAL available + +export module boost.xyz; +// ... + +``` + +Most already modularized Boost dependencies don't need any change. +You just need to add the relevant include: + +```cpp +// +// header.hpp +// +#include // doesn't need to be modified + +// ... + +// +// boost_xyz.cppm +// + +// ... +export module boost.xyz; +import boost.core; // needs to be added because we use + +// ... +``` + +Like for the standard library, some Boost headers export mainly macros. +These need to be included in the global module fragment, too. +Unlike the standard library headers, these Boost headers will +emit a clear error if you use them in the purview, but don't include +them in the global module fragment. The `BOOST_IN_MODULE_PURVIEW` +macro is used to detect this. + +For example: + +```cpp +// +// header.hpp +// +// None of these need modification here, but need to be included in the GMF +#include +#include +#include + +// ... + +// +// boost_xyz.cppm +// +module; + +#include +#include +#include + +export module boost.xyz; +// ... +``` + +Finally, if you have any other dependencies, you need to ifdef-out them yourself. +For example, say that your library includes `` in a header. +You'd need to do the following: + +```cpp +// +// header.hpp +// +#ifndef BOOST_USE_MODULES +#include +#endif + +// ... + +// +// boost_xyz.cppm +// +module; + +#include + +export module boost.xyz; +// ... +``` + +You can also create a small wrapper header to avoid cluttering. + + + + +* Tests: set(STD_HEADER) + +* + + * In a config header, define a macro BOOST_XYZ_MODULE_EXPORT that expands to either `export` or to nothing, + depending on whether `BOOST_USE_MODULES` is defined or not. + * Stick `BOOST_XYZ_MODULE_EXPORT` to all public identifiers (footnote: no template specializations) + that the library exports. + * + + + +## Design decisions + +* Why ABI breaking vs other approaches? + * Vs. export using: because it works. export using (TBC: include an example) looks attractive because + it's non-intrusive. But once you try to build enough of your test suite with it, you realize it doesn't + work. This is because of GMF discards: there are a number of corner cases (TBC: link to modules4) + where entities are discarded and change the meaning of the program + * The tuple protocol + * Template specializations + * Vs. non-ABI-breaking: because it's easier for the compiler to diagnose ODR violations. + The cost is not being able to mix include/import. + TBC: expand this argument. +* Why using a static library in CMake + + Some libraries don't get a module at all (e.g. Boost.Config because it exports only macros). More on this later. + +Libraries that only export macros don't get a module. + + From f5763b819214a84e911f9b59c765d6311c0dcffb Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 5 Mar 2026 20:48:32 +0100 Subject: [PATCH 10/19] Modules docs 2 --- modules.md | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 7 deletions(-) diff --git a/modules.md b/modules.md index 996f7a4..a1fe796 100644 --- a/modules.md +++ b/modules.md @@ -314,19 +314,146 @@ export module boost.xyz; You can also create a small wrapper header to avoid cluttering. +### Creating the compatibility headers + +We should now have to go over all our public headers and apply appropriate preprocessor +magic to make them "compatibility headers". For the sake of simplicity, let's consider +headers that don't export macros first. + +It is useful to think how do we want our headers to behave in different contexts +(always with `BOOST_USE_MODULES` defined): + +1. In non-modular code: translate to `import boost.xyz` and nothing else. + An example of this case is the `main.cpp` file of an executable. + The global module fragment shares the same characteristics as non-modular code. +2. In the purview of our own module: leave the header as-is. + We need all of our declarations intact so we can export them. +3. In the purview of other modules: translate to nothing. + In modules, `import` needs to happen either in the global module fragment, + or immediately after `export module boost.xyz`. This means that we need + to disable the import in purviews, or we will generate errors to consumers. + +With this scheme in mind, our public headers become: + +```cpp +// include guards omitted + +#if defined(BOOST_USE_MODULES) && !defined(BOOST_XYZ_INTERFACE_UNIT) + +#ifndef BOOST_IN_MODULE_PURVIEW +import boost.core; +#endif + +#else + +// declarations here + +#endif +``` + +The idea is that all Boost modules define `BOOST_IN_MODULE_PURVIEW` when their purview +begins, and we use this to disable the import. If you double-check, we're already +defining this macro in `boost_xyz.cppm`. `BOOST_XYZ_INTERFACE_UNIT` is only defined +in `boost_xyz.cppm`, and used to distinguish case 1 in the list above. + +### Headers that export macros + +If your header exports public macros, the compatibility headers needs to make these available. +In the simplest case, your macros don't depend on other macros, and this is trivial. +Consider a `BOOST_XYZ_VERSION` macro: + +```cpp +// +// boost/xyz/version.hpp +// +// include guards omitted + +// BOOST_XYZ_VERSION doesn't require including any other header. +// This header doesn't need any changes. +#define BOOST_XYZ_VERSION 1_91_0 + +// +// boost/xyz/header.hpp +// +// include guards omitted + +#if defined(BOOST_USE_MODULES) && !defined(BOOST_XYZ_INTERFACE_UNIT) + +// This header makes available BOOST_XYZ_VERSION, too +#include // safe to include in purviews +#ifndef BOOST_IN_MODULE_PURVIEW +import boost.core; +#endif + +#else + +// declarations here + +#endif + +``` + +However, there is a big chance that your macros are expressed in terms +of other macros. If you need a third-party include that might also declare +C++ entities, your header is no longer suitable to be used in module purviews. + +For example, consider this header: + +```cpp +// +// boost/xyz/config.hpp +// + +#ifndef BOOST_XYZ_CONFIG_HPP +#define BOOST_XYZ_CONFIG_HPP + +#include // LDBL_MANT_DIG and LDBL_MAX_EXP + +// BOOST_XYZ_SUPPORTS_LONG_DOUBLE indicates a supported feature, and is a documented macro +#if LDBL_MANT_DIG == 64 && LDBL_MAX_EXP == 16384 +# define BOOST_XYZ_SUPPORTS_LONG_DOUBLE +#endif + +#endif +``` + +This header is usable as-is in case 1 (non-modular code), +but can't be used in purviews (case 2 and 3) because any names defined by `` +would end up attached to your module. + +Your best chance is to try to detect misuse and issue an error: + +```cpp +// +// boost/xyz/config.hpp +// + +// Detect misuse +#if defined(BOOST_IN_MODULE_PURVIEW) && !defined(BOOST_XYZ_CONFIG_HPP) +# error "Please #include in your module global fragment" +#endif + +#ifndef BOOST_XYZ_CONFIG_HPP +#define BOOST_XYZ_CONFIG_HPP + +#include // Stays as is - don't replace by the compatibility header! + +#if LDBL_MANT_DIG == 64 && LDBL_MAX_EXP == 16384 +# define BOOST_XYZ_SUPPORTS_LONG_DOUBLE +#endif + +#endif +``` + +Headers like ``, `` and `` +use this technique. + * Tests: set(STD_HEADER) -* - * In a config header, define a macro BOOST_XYZ_MODULE_EXPORT that expands to either `export` or to nothing, - depending on whether `BOOST_USE_MODULES` is defined or not. - * Stick `BOOST_XYZ_MODULE_EXPORT` to all public identifiers (footnote: no template specializations) - that the library exports. - * - ## Design decisions From 29ef8904244e37323c52f4552d581b0ab5df5f90 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Thu, 5 Mar 2026 20:57:27 +0100 Subject: [PATCH 11/19] Running the test suite --- modules.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/modules.md b/modules.md index a1fe796..dd56911 100644 --- a/modules.md +++ b/modules.md @@ -448,13 +448,35 @@ Your best chance is to try to detect misuse and issue an error: Headers like ``, `` and `` use this technique. +### Running the test suite +Running a big part of the library's test suite is critical to guarantee correctness. +Module support is still clunky under some compilers. It is also easy +to forget an export macro. +You should run all tests that target the library's public API. +This way, if you forgot an export, or commented a header in an unintended way, you will know. -* Tests: set(STD_HEADER) +While testing implementation details is possible, it is more trouble than +is worth, since functionality itself should be tested already in non-modular builds. + +The only change that should be required to your tests is replacing +the standard library includes by the compatibility headers in Boost.Config. +You can use `BOOST_USE_MODULES` to ifdef-out tests targeting the private API. + +Additionally, you need to enable `import std` in your tests by modifying your `test/CMakeLists.txt`: + +```cmake +# ... +if(BOOST_USE_MODULES) + set(CMAKE_CXX_MODULE_STD ON) +endif() + +# add your tests here +``` + +This is required because `CMAKE_CXX_MODULE_STD` doesn't propagate to dependent targets. - - ## Design decisions From 0df3ae68e55df2f0ab8fe9e2fcbaba35131ca867 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 18:40:55 +0100 Subject: [PATCH 12/19] Compiled libs 1 --- modules.md | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/modules.md b/modules.md index dd56911..a449562 100644 --- a/modules.md +++ b/modules.md @@ -477,6 +477,101 @@ endif() This is required because `CMAKE_CXX_MODULE_STD` doesn't propagate to dependent targets. +## Compiled libraries + +Compiled libraries are slightly more involved. All the steps already described apply, +plus some extra steps described here. + +Most `.cpp` files in our library need to either use or implement private functionality +not exported by the module. For this to work, they need to be made part of the module. +Using the preprocessor doesn't help here (TODO: link). + +We recommend creating a separate file with a different extension for each `.cpp` file +in your library. For example, given an `utils.cpp` file, you might create an `utils.cc` +file like the following: + + +```cpp +// utils.cc + +module; + +// Place here any includes providing macros required by your cpp file +#include +#include + +// This is a module implementation unit +module boost.xyz; + +// Include any modular dependencies here +import std; +import boost.core; + +// We're in a purview. Compatibility headers shouldn't generate purviews +#define BOOST_IN_MODULE_PURVIEW + +// Just include the non-modular code +#include "utils.cpp" +``` + +For this to work, you need to make sure that your `.cpp` files +don't include any third-party names that might get attached to our module. +You should follow the same procedure as with headers. + +Note that, by the rules of C++20 modules, your `.cc` files automatically +import all the names defined in your primary module interface. + +Because module interface units can't be imported, they should be added +to CMake like regular sources, rather than to the `CXX_MODULES` file set. +For instance: + +```cmake +# Add the regular translation units +if (BOOST_USE_MODULES) + set(SOURCE_SUFFIX "cc") +else() + set(SOURCE_SUFFIX "cpp") +endif() + +add_library(boost_xyz + src/utils.${SOURCE_SUFFIX} +) + +# Now add the module primary interface unit +if (BOOST_USE_MODULES) + target_sources(boost_xyz PUBLIC FILE_SET CXX_MODULES BASE_DIRS modules FILES modules/boost_xyz.cppm) + # ... +endif() +``` + +### Guarding detail headers + +If your `.cpp` files include any `detail/` headers directly, you need to +guard these to avoid double definitions. These definitions will be already +present in the primary module interface, and shouldn't appear again +in the module implementation units. + +The simplest way is to use an approach similar to public headers: + +```cpp +// +// File boost/xyz/detail/utils.hpp +// + +// Include guards omitted. +// Make the header a no-op outside the primary module interface +#if !defined(BOOST_USE_MODULES) || defined(BOOST_XYZ_INTERFACE_UNIT) + +// Header contents + +#endif +``` + +### Source-only headers + + +boost_xyz_interface.cppm that exports the interface, to workaround gcc bugs + ## Design decisions @@ -491,9 +586,33 @@ This is required because `CMAKE_CXX_MODULE_STD` doesn't propagate to dependent t The cost is not being able to mix include/import. TBC: expand this argument. * Why using a static library in CMake +* Why not using the preprocessor to make `.cpp` files module units conditionally. + The most straightforward idea would be to write the following: + +```cpp - Some libraries don't get a module at all (e.g. Boost.Config because it exports only macros). More on this later. +// This DOES NOT WORK, because module; and module boost.xyz; need to be the first +// declarations in the translation unit + +#ifdef BOOST_USE_MODULES +module; +#endif -Libraries that only export macros don't get a module. +// Global module fragment includes +#include +#include + +#ifdef BOOST_USE_MODULES +module boost.xyz; +#endif + +// Content of the .cpp file as is today +#include + +int boost::xyz::f() { /* ... */ } + +``` +This doesn't work because the module declarations need to be the first things to happen +in the translation unit. This limitation probably makes dependency scanning more efficient. From b757c781c2c32d654a0d2b45e6e6bba5bcb25543 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 19:07:21 +0100 Subject: [PATCH 13/19] src headers --- modules.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/modules.md b/modules.md index a449562..0d61e28 100644 --- a/modules.md +++ b/modules.md @@ -492,7 +492,9 @@ file like the following: ```cpp -// utils.cc +// +// File: utils.cc +// module; @@ -569,6 +571,91 @@ The simplest way is to use an approach similar to public headers: ### Source-only headers +Functionality shared by several `.cpp` files is usually placed +into header files that live within the `src/` directory. These headers +are "source-only", in the sense that they don't get installed like the ones under the `include/` directory. + +To avoid redefinition errors (TODO: link to rationale), we need to wrap these headers into module partition implementation units (partitions not marked with `export`). For example, given the header `base64.hpp`, we can create a `base64.cppm` file with the following contents: + +```cpp +// +// File: base64.cppm +// +module; + +// Place any macro-related includes that base64.hpp may need +#include + +// This is a partition +module boost.xyx:base64; + +// We need to access all the public and private functionality +// defined in our headers. Partitions don't import the primary +// interface unit by default, so this is needed. +// gcc-15 currently has a bug with this - see later for a workaround +import boost.xyz; + +// Similar to BOOST_XYZ_INTERFACE_UNIT, used to guard the file +#define BOOST_XYZ_BASE64_PARTITION_UNIT + +#include "base64.hpp" +``` + +The header file needs to be guarded so it expands to nothing when +included in the `.cpp` files: + +```cpp +// +// File: base64.hpp +// + +// Include guards omitted. +// Make the header a no-op outside base64.cppm +#if !defined(BOOST_USE_MODULES) || defined(BOOST_XYZ_BASE64_PARTITION_UNIT) + +// Header contents + +#endif +``` + +Finally, module units can import the partitions. Imagine that `utils.cc` +includes `base64.hpp` in its code: + +```cpp +// +// File: utils.cc +// + +module; + +// Global module fragment + +module boost.xyz; + +import :base64; // Import the partition +import std; +import boost.core; + +// Purview as was before +#define BOOST_IN_MODULE_PURVIEW +#include "utils.cpp" +``` + +Partition units need to be compiled. Since they can be imported, +they need to be in a CMake `CXX_MODULES` file set. Because they +are internal to the library and shouldn't be installed to the user, +they should be part of a `PRIVATE` file set: + +```cmake +target_sources(boost_xyz + PUBLIC FILE_SET CXX_MODULES BASE_DIRS modules FILES + modules/boost_xyz.cppm + PRIVATE FILE_SET source_headers TYPE CXX_MODULES BASE_DIRS src FILES + src/base64.cppm +) +``` + + boost_xyz_interface.cppm that exports the interface, to workaround gcc bugs @@ -588,6 +675,9 @@ boost_xyz_interface.cppm that exports the interface, to workaround gcc bugs * Why using a static library in CMake * Why not using the preprocessor to make `.cpp` files module units conditionally. The most straightforward idea would be to write the following: +* Why can't headers be used directly in cpp files? + If we include one of these headers into several translation units, we will get redefinition + errors. We can't include them in the GMF because they need access to the module private content. ```cpp From bece0c7e0b68d5b0248d26358184f8292244113a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 19:15:59 +0100 Subject: [PATCH 14/19] gcc15 workarounds --- modules.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/modules.md b/modules.md index 0d61e28..a1f7446 100644 --- a/modules.md +++ b/modules.md @@ -655,10 +655,59 @@ target_sources(boost_xyz ) ``` +### Workarounds for gcc-15 +GCC 15 has [a bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124309) +that causes errors when explicitly importing the primary module interface +from partitions. -boost_xyz_interface.cppm that exports the interface, to workaround gcc bugs +To workaround this problem, we can place all exported functionality +in a module interface partition, and make the primary interface unit +a wrapper: +```cpp +// +// File: boost_xyz_interface.cppm +// + +// This file has all the contents that boost_interface.cppm used to have, +// except for the module identifier +module; + +// Global module fragment +#include + +export module boost.xyz:interface; // partition + +#include + +// +// File: boost_xyz.cppm +// + +// Simply re-export the partition +export module boost.xyz; +export import :interface; + +// +// File: base64.cppm +// +module; + +#include + +module boost.xyx:base64; + +// We've replaced 'import boost.xyz' by this +import :interface; + +// Rest of the file unmodified +#define BOOST_XYZ_BASE64_PARTITION_UNIT +#include "base64.hpp" +``` + +The new `boost_xyz_interface.cppm` should be placed in the public +file set in CMake. ## Design decisions From 6acac98f7d5330ea5880487ac057cd32c45d30fa Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 21:07:54 +0100 Subject: [PATCH 15/19] Rephrasing --- modules.md | 474 ++++++++++++++++++++++++++++------------------------- 1 file changed, 247 insertions(+), 227 deletions(-) diff --git a/modules.md b/modules.md index a1f7446..cfbf663 100644 --- a/modules.md +++ b/modules.md @@ -1,75 +1,79 @@ # C++20 modules in Boost -Audience: users that want to consume Boost as C++20 modules. -Library authors that want to support C++20 modules in their libraries. - -Guidelines to add C++20 module support for existing Boost libraries. - +This document is for users who want to consume Boost as C++20 modules, +and for library authors who want to add module support to their libraries. ## Goals -* Code should be usable as a module ('import boost.xyz') and as headers. - Existing code should not break. -* Feature-completeness. When consuming a library as a module, the entire +* **Backwards-compatibility**. Code should be usable as a module (`import boost.xyz`) and as headers. + Existing code must not break. +* **Feature-completeness**. When consuming a library as a module, the entire library should be usable and work correctly. -* Focus on compile times. Consuming Boost as a module should be able to reduce - compile times as much as possible. -* Isolation. Importing a library should pour only bring in the public identifiers. -* Maintainability. Modular bindings add clutter to the code. We aim at getting - as less clutter as possible. -* Ease of use with CMake. Users need to be able to consume Boost with C++20 modules - with CMake with the same easiness as they do without modules. - No manual building of .ixx files should happen. - -Non-goals - -* Mixing including and importing Boost. Boost will still work regularly using - headers. If you encounter this need, just use headers. -* Supporting toolchains with partial modular support. - We're currently targeting the latest toolchain versions, and these still contain - bugs. -* Supporting toolchains with modules but no import std. Import std is crucial - for builds performance. We only support configurations that allow importing std. +* **Compile time improvements**. Consuming Boost as a module should reduce compile times + as much as possible. +* **Isolation**. Importing a library should only bring in its public identifiers. +* **Maintainability**. Modular bindings add clutter to the code. We aim at + keeping it to a minimum. +* Ease of use with **CMake**. Users should be able to consume Boost with + C++20 modules as easily as they do without them today. No manual building + of `.cppm` files should be necessary. + +Non-goals: + +* Mixing including and importing Boost. If you need this, just use headers. +* Supporting toolchains with partial module support. + The latest toolchain versions still contain bugs that need workarounds. + We aim to remove these workarounds once the tools improve. +* Supporting toolchains with modules but no `import std`. This feature + is crucial for build performance. We only support configurations + that allow importing `std`. ## User interface -* To use Boost with modules, users need to build it with CMake, enabling support for it explicitly. - This is done by specifying the `-DBOOST_USE_MODULES=1` CMake option when building Boost. -* Users can consume this built version of Boost either by using `add_subdirectory`, - or by installing it and then using `find_package`. -* Each Boost library gets its own C++20 module. For example, Boost.Mp11 can be consumed with `import boost.mp11`. -* When consuming a Boost distribution that uses modules, all public Boost headers are translated into their - corresponding import, plus any documented macro definition. - This is done using the preprocessor and defining the `BOOST_USE_MODULES`, which happens automatically when using CMake. - We call this "compatibility headers". +Users need to consume Boost via CMake, enabling module support by +specifying `-DBOOST_USE_MODULES=1`. Users may either consume Boost +directly using `add_subdirectory`, or build it first, install it, +and using `find_package`. (TODO) + +Each Boost library gets its own C++20 module. For example, Boost.Mp11 +can be consumed with `import boost.mp11`. (TODO: exceptions) + +When consuming a Boost distribution built with modules, all public +Boost headers are translated into their corresponding `import`, plus +any documented macro definitions. This is done using preprocessor logic +and the `BOOST_USE_MODULES` macro. +We call these "compatibility headers". ## Modularizing header-only libraries -At this point in time, modularizing a library requires modularizing its dependencies. -This can probably be lifted, but has not been researched yet (and order gives better build-time performance results). +At this point, modularizing a library requires modularizing its +dependencies first. This restriction can probably be lifted, but +hasn't been researched yet (and dependency order yields better +build-time performance). -The recommended approach is the "ABI breaking" style. The idea is marking all the library public entities with -a `BOOST_XYZ_MODULE_EXPORT` macro that expands to `export` when building with modules, then include -all the library public headers in the module purview. +The recommended approach is the "ABI breaking" style (TODO: link): mark all public +entities with a `BOOST_XYZ_MODULE_EXPORT` macro that expands to +`export` when building with modules, then include all public headers +in the module purview. -The rest of this section is a step-by-step guide on how to create the modular interface. +The rest of this section is a step-by-step guide on how to achieve this. ### Primary interface unit -Your library should contain a single module unit, and it should be a primary interface unit. -For an hypothetical Boost.Xyz, it should be named `boost_xyz.cppm` and placed under the -`modules/` folder. +Your library should contain a single module unit: the primary interface +unit. For a hypothetical Boost.Xyz, it should be named `boost_xyz.cppm` +and placed under the `modules/` directory. -This is what it should contain in a first iteration (we will add things later): +A first iteration looks like this (we will add things later): ```cpp -// Global module fragment empty for now. We will add things later here +// Global module fragment. Empty for now. module; // Begins the module purview export module boost.xyz; -// Include any dependency that your library might have +// Include here any dependency that your library has import std; import boost.core; @@ -77,23 +81,22 @@ import boost.core; #define BOOST_XYZ_INTERFACE_UNIT #define BOOST_IN_MODULE_PURVIEW -// Including headers with #include <> triggers warnings. +// Including headers with #include <> in the purview triggers warnings. // If you're following this guide, they should be safe to ignore. // This header disables them #include -// Now include your library. Add any other header that exports symbols +// Include the entire library #include ``` -We'll get more in-depth into why each statement is required later. -For now, have in mind that we're just including our entire library in the module purview. +We're including our entire library in the module purview. The reasons +for each other line will become clear as you read on. ### CMake code -Next, we need to add code to our `CMakeLists.txt` to that CMake -knows how to build our code. If you're following Boost best practice, -your code may look like: +Next, we need CMake to know how to build our module. Starting from +a typical Boost `CMakeLists.txt`: ```cmake cmake_minimum_required(VERSION 3.5...3.31) @@ -111,12 +114,10 @@ target_link_libraries(boost_xyz ) ``` -Module units are translation units, so our when using modules, -our library will no longer be an INTERFACE library. -A small binary will get generated, containing the module initializer only. -(TODO: can we make this a footnote?) This is a function that initializes -global variables. It should be a no-op in most header-only libraries. -This is what the CMake code looks like after the change: +Module units are translation units, so when using modules, the library +is no longer an `INTERFACE` library. A small binary is generated containing only +the module initializer (a function that initializes global variables, +typically a no-op for header-only libraries) (TODO: expand). The updated CMake code: ```cmake if (BOOST_USE_MODULES) @@ -141,20 +142,22 @@ target_link_libraries(boost_xyz ) ``` -* We use a STATIC library (vs. not specifying a type and letting the user choose) - because the binary is expected to contain almost no code. - It should make initialization more efficient and simplify deployment. -* We use `__scope` because it's an error to use `PUBLIC` with an interface library. -* `modules/boost_xyz.cppm` will be installed alongside the produced binary. - This happens automatically. This is required because BMIs must be regenerated - by consumers. +Some notes: + +* We use `STATIC` (rather than letting the user choose) because the + binary is expected to contain almost no code. This simplifies + deployment and makes initialization more efficient. +* We use `__scope` because using `PUBLIC` properties with an interface library is an error. +* `modules/boost_xyz.cppm` is installed alongside the produced binary + automatically by the Boost.CMake infrastructure. This is required because binary module interfaces (BMIs) must be regenerated by + consumers. ### Marking exported entities -By default, entities defined in the module purview are not exported, -and won't be visible to importers. We need to mark public names with the `export` keyword. -To maintain compatibility, we advise to define a macro like this: +By default, entities defined in the module purview are not exported and +won't be visible to importers. We need to mark public names with the +`export` keyword. To maintain compatibility, define a macro: ```cpp // For example, in boost/xyz/detail/config.hpp @@ -165,51 +168,44 @@ To maintain compatibility, we advise to define a macro like this: #endif ``` -We should now stick `BOOST_XYZ_MODULE_EXPORT` in front of every entity exported by the library. +Place `BOOST_XYZ_MODULE_EXPORT` in front of every entity exported by +the library. -For templates, only the primary template should be exported. -Specializations should not. This means that if you're specializing -a template from the standard library, you don't need to export it. -It will be made available to importers by the compiler automatically. +When dealing with templates, only the primary template should be exported, and not its specializations. +If you specialize a template from the standard library, don't export it. +The compiler makes it available to importers automatically. ### Disabling includes for dependencies -As you might know, anything included in the module purview -will get attached to our `boost.xyz` named module. -Only the names that our library exports should be attached there, -and not the names defined by our dependencies. -This is why compilers warn when using `#include <>` in the module -purview. +Anything included in the module purview gets attached to our `boost.xyz` +named module. Only names that belong to our library should be attached. +Names declared by our dependencies shouldn't. This is why compilers warn when using +`#include <>` in the module purview. -While you can manually add `#ifdef` blocks for every dependency, -this is usually error-prone. For the standard library, Boost.Config -offers "compatibility headers" that will become a no-op when -building with modules. +While you can manually add `#ifdef` blocks for every dependency, this is +error-prone. For the standard library, Boost.Config offers compatibility +headers that become no-ops when building with modules. -For instance, if a header contains this: +For instance, if a header contains: ```cpp -// ... #include #include -// ... ``` You can write: ```cpp -// ... #include #include -// ... ``` -This will expand to nothing in our purview, and is equivalent to -the former when `BOOST_USE_MODULES` is not defined. +This expands to nothing in the module purview, and is equivalent to the +original when `BOOST_USE_MODULES` is not defined. -Some standard library headers also export macros. Boost.Config -compatibility headers don't export any macros. If you need the macros, -you need to include the original header in the global module fragment: +Some standard library headers also export macros. Boost.Config's +compatibility headers don't export any. If you need the macros, include +the original header in the global module fragment: ```cpp // @@ -232,14 +228,14 @@ export module boost.xyz; ``` -Most already modularized Boost dependencies don't need any change. -You just need to add the relevant include: +Most already-modularized Boost dependencies don't need any changes. +Just add the relevant import: ```cpp // // header.hpp // -#include // doesn't need to be modified +#include // no modifications needed // ... @@ -249,17 +245,16 @@ You just need to add the relevant include: // ... export module boost.xyz; -import boost.core; // needs to be added because we use +import boost.core; // needed because we use // ... ``` -Like for the standard library, some Boost headers export mainly macros. -These need to be included in the global module fragment, too. -Unlike the standard library headers, these Boost headers will -emit a clear error if you use them in the purview, but don't include -them in the global module fragment. The `BOOST_IN_MODULE_PURVIEW` -macro is used to detect this. +Like the standard library, some Boost headers export mainly macros. +These need to be included in the global module fragment. Unlike +standard library headers, these Boost headers emit a clear error if +used in the purview without being included in the global module fragment +first. The `BOOST_IN_MODULE_PURVIEW` macro is used to detect this. For example: @@ -268,7 +263,7 @@ For example: // header.hpp // // None of these need modification here, but need to be included in the GMF -#include +#include #include #include @@ -279,7 +274,7 @@ For example: // module; -#include +#include #include #include @@ -287,9 +282,9 @@ export module boost.xyz; // ... ``` -Finally, if you have any other dependencies, you need to ifdef-out them yourself. -For example, say that your library includes `` in a header. -You'd need to do the following: +Finally, for other dependencies (e.g. platform headers), you need to +ifdef them out yourself. For example, if your library includes +``: ```cpp // @@ -312,28 +307,29 @@ export module boost.xyz; // ... ``` -You can also create a small wrapper header to avoid cluttering. +You can also create a small wrapper header to avoid cluttering your +code. ### Creating the compatibility headers -We should now have to go over all our public headers and apply appropriate preprocessor -magic to make them "compatibility headers". For the sake of simplicity, let's consider -headers that don't export macros first. +We now need to turn all public headers into compatibility headers. +Headers that don't export macros are simpler, so we'll cover those +first. -It is useful to think how do we want our headers to behave in different contexts -(always with `BOOST_USE_MODULES` defined): +It is useful to consider how headers should behave in different +contexts (always with `BOOST_USE_MODULES` defined): -1. In non-modular code: translate to `import boost.xyz` and nothing else. - An example of this case is the `main.cpp` file of an executable. - The global module fragment shares the same characteristics as non-modular code. -2. In the purview of our own module: leave the header as-is. - We need all of our declarations intact so we can export them. -3. In the purview of other modules: translate to nothing. - In modules, `import` needs to happen either in the global module fragment, - or immediately after `export module boost.xyz`. This means that we need - to disable the import in purviews, or we will generate errors to consumers. +1. **In non-modular code**: translate to `import boost.xyz` and nothing + else. This would happen in the `main.cpp` of an executable, for instance. The global + module fragment has the same characteristics as non-modular code. +2. **In the purview of our own module**: leave the header as-is. + We need all declarations intact to export them. +3. **In the purview of other modules**: translate to nothing. In + modules, `import` must happen either in the global module fragment + or immediately after `export module`. Using it elsewhere generates + errors. This means that we must disable the `import` in purviews. -With this scheme in mind, our public headers become: +With this scheme, our public headers become: ```cpp // include guards omitted @@ -351,16 +347,17 @@ import boost.core; #endif ``` -The idea is that all Boost modules define `BOOST_IN_MODULE_PURVIEW` when their purview -begins, and we use this to disable the import. If you double-check, we're already -defining this macro in `boost_xyz.cppm`. `BOOST_XYZ_INTERFACE_UNIT` is only defined -in `boost_xyz.cppm`, and used to distinguish case 1 in the list above. +All Boost modules define `BOOST_IN_MODULE_PURVIEW` when their purview +begins, which disables the import. If you double-check, we already +define this macro in `boost_xyz.cppm`. `BOOST_XYZ_INTERFACE_UNIT` is +only defined in `boost_xyz.cppm`, and distinguishes case 2 from the +others. ### Headers that export macros -If your header exports public macros, the compatibility headers needs to make these available. -In the simplest case, your macros don't depend on other macros, and this is trivial. -Consider a `BOOST_XYZ_VERSION` macro: +If a header exports public macros, the compatibility header needs to +make them available. In the simplest case, the macros don't depend on +other macros. Consider a `BOOST_XYZ_VERSION` macro: ```cpp // @@ -393,11 +390,11 @@ import boost.core; ``` -However, there is a big chance that your macros are expressed in terms -of other macros. If you need a third-party include that might also declare -C++ entities, your header is no longer suitable to be used in module purviews. +However, there is a good chance that your macros depend on other macros. +If you need a third-party include that also declares C++ entities, your +header is no longer suitable for use in module purviews. -For example, consider this header: +For example: ```cpp // @@ -417,11 +414,11 @@ For example, consider this header: #endif ``` -This header is usable as-is in case 1 (non-modular code), -but can't be used in purviews (case 2 and 3) because any names defined by `` -would end up attached to your module. +This header works in non-modular code, but can't be used in purviews +because any names defined by `` would get attached to your +module. -Your best chance is to try to detect misuse and issue an error: +The best approach is to detect misuse and issue an error: ```cpp // @@ -445,26 +442,27 @@ Your best chance is to try to detect misuse and issue an error: #endif ``` -Headers like ``, `` and `` -use this technique. +Headers like ``, `` and +`` use this technique. ### Running the test suite -Running a big part of the library's test suite is critical to guarantee correctness. -Module support is still clunky under some compilers. It is also easy -to forget an export macro. +Running a large part of the library's test suite is critical for +correctness. Module support is still clunky under some compilers, and +it is easy to forget an export macro. -You should run all tests that target the library's public API. -This way, if you forgot an export, or commented a header in an unintended way, you will know. +Run all tests that target the library's public API. If you forgot an +export or changed a header in an unintended way, you will know. -While testing implementation details is possible, it is more trouble than -is worth, since functionality itself should be tested already in non-modular builds. +Testing implementation details is possible but not worth the effort, +since functionality is already tested in non-modular builds. -The only change that should be required to your tests is replacing -the standard library includes by the compatibility headers in Boost.Config. -You can use `BOOST_USE_MODULES` to ifdef-out tests targeting the private API. +The only change required to your tests is replacing standard library +includes with the Boost.Config compatibility headers. You can use +`BOOST_USE_MODULES` to ifdef-out tests targeting the private API. -Additionally, you need to enable `import std` in your tests by modifying your `test/CMakeLists.txt`: +Additionally, enable `import std` in your tests by modifying +`test/CMakeLists.txt`: ```cmake # ... @@ -475,20 +473,21 @@ endif() # add your tests here ``` -This is required because `CMAKE_CXX_MODULE_STD` doesn't propagate to dependent targets. +This is required because `CMAKE_CXX_MODULE_STD` doesn't propagate to +dependent targets. ## Compiled libraries -Compiled libraries are slightly more involved. All the steps already described apply, -plus some extra steps described here. +All the steps described above apply to compiled libraries, plus some +additional ones described here. -Most `.cpp` files in our library need to either use or implement private functionality -not exported by the module. For this to work, they need to be made part of the module. -Using the preprocessor doesn't help here (TODO: link). +Most `.cpp` files need to use or implement private functionality not +exported by the module. For this to work, they need to be part of the +module. Using the preprocessor doesn't help here (see +[Design decisions](#design-decisions)). -We recommend creating a separate file with a different extension for each `.cpp` file -in your library. For example, given an `utils.cpp` file, you might create an `utils.cc` -file like the following: +We recommend creating a separate file with a different extension for +each `.cpp` file. For example, given `utils.cpp`, create `utils.cc`: ```cpp @@ -498,34 +497,33 @@ file like the following: module; -// Place here any includes providing macros required by your cpp file +// Place here any includes providing macros required by the cpp file #include #include // This is a module implementation unit module boost.xyz; -// Include any modular dependencies here +// Import modular dependencies import std; import boost.core; -// We're in a purview. Compatibility headers shouldn't generate purviews +// We're in a purview. Compatibility headers shouldn't generate imports #define BOOST_IN_MODULE_PURVIEW -// Just include the non-modular code +// Include the non-modular code #include "utils.cpp" ``` -For this to work, you need to make sure that your `.cpp` files -don't include any third-party names that might get attached to our module. -You should follow the same procedure as with headers. +For this to work, your `.cpp` files must not include any third-party +names that might get attached to the module. Follow the same procedure +as with headers to achieve this. -Note that, by the rules of C++20 modules, your `.cc` files automatically -import all the names defined in your primary module interface. +Note that by the rules of C++20 modules, your `.cc` files automatically +import all names defined in the primary module interface. -Because module interface units can't be imported, they should be added -to CMake like regular sources, rather than to the `CXX_MODULES` file set. -For instance: +Because module implementation units can't be imported, they should be +added to CMake as regular sources, rather than to the `CXX_MODULES FILE_SET`: ```cmake # Add the regular translation units @@ -548,12 +546,12 @@ endif() ### Guarding detail headers -If your `.cpp` files include any `detail/` headers directly, you need to -guard these to avoid double definitions. These definitions will be already -present in the primary module interface, and shouldn't appear again -in the module implementation units. +If your `.cpp` files include any `detail/` headers directly, guard +these to avoid double definitions. The definitions are already present +in the primary module interface and shouldn't appear again in the +module implementation units. -The simplest way is to use an approach similar to public headers: +Use an approach similar to public headers: ```cpp // @@ -571,11 +569,14 @@ The simplest way is to use an approach similar to public headers: ### Source-only headers -Functionality shared by several `.cpp` files is usually placed -into header files that live within the `src/` directory. These headers -are "source-only", in the sense that they don't get installed like the ones under the `include/` directory. +Functionality shared by several `.cpp` files is usually placed into +header files that live within `src/`. These headers are "source-only": +they don't get installed like the ones under `include/`. -To avoid redefinition errors (TODO: link to rationale), we need to wrap these headers into module partition implementation units (partitions not marked with `export`). For example, given the header `base64.hpp`, we can create a `base64.cppm` file with the following contents: +To avoid redefinition errors (see [Design decisions](#design-decisions)), +wrap these headers into module implementation partition units (partitions +not marked with `export`). For example, given `base64.hpp`, create +`base64.cppm`: ```cpp // @@ -587,12 +588,11 @@ module; #include // This is a partition -module boost.xyx:base64; +module boost.xyz:base64; -// We need to access all the public and private functionality -// defined in our headers. Partitions don't import the primary -// interface unit by default, so this is needed. -// gcc-15 currently has a bug with this - see later for a workaround +// We need access to all public and private functionality defined in our +// headers. Partitions don't import the primary interface unit by default. +// gcc-15 currently has a bug with this - see below for a workaround import boost.xyz; // Similar to BOOST_XYZ_INTERFACE_UNIT, used to guard the file @@ -601,8 +601,8 @@ import boost.xyz; #include "base64.hpp" ``` -The header file needs to be guarded so it expands to nothing when -included in the `.cpp` files: +Guard the header file so it expands to nothing when included in +`.cpp` files: ```cpp // @@ -618,8 +618,8 @@ included in the `.cpp` files: #endif ``` -Finally, module units can import the partitions. Imagine that `utils.cc` -includes `base64.hpp` in its code: +Module units can then import the partitions. If `utils.cc` includes +`base64.hpp`: ```cpp // @@ -636,15 +636,14 @@ import :base64; // Import the partition import std; import boost.core; -// Purview as was before +// Purview as before #define BOOST_IN_MODULE_PURVIEW #include "utils.cpp" ``` -Partition units need to be compiled. Since they can be imported, -they need to be in a CMake `CXX_MODULES` file set. Because they -are internal to the library and shouldn't be installed to the user, -they should be part of a `PRIVATE` file set: +Partition units need to be compiled. Since they can be imported, they +must be in a CMake `CXX_MODULES FILE_SET`. Because they are internal +and shouldn't be installed, they should be `PRIVATE`: ```cmake target_sources(boost_xyz @@ -658,19 +657,18 @@ target_sources(boost_xyz ### Workarounds for gcc-15 GCC 15 has [a bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124309) -that causes errors when explicitly importing the primary module interface -from partitions. +that causes errors when explicitly importing the primary module +interface from partitions. -To workaround this problem, we can place all exported functionality -in a module interface partition, and make the primary interface unit -a wrapper: +The workaround is to place all exported functionality in a module +interface partition, making the primary interface unit a thin wrapper: ```cpp // // File: boost_xyz_interface.cppm // -// This file has all the contents that boost_interface.cppm used to have, +// Contains everything that boost_xyz.cppm used to have, // except for the module identifier module; @@ -685,7 +683,7 @@ export module boost.xyz:interface; // partition // File: boost_xyz.cppm // -// Simply re-export the partition +// Re-export the partition export module boost.xyz; export import :interface; @@ -696,9 +694,9 @@ module; #include -module boost.xyx:base64; +module boost.xyz:base64; -// We've replaced 'import boost.xyz' by this +// We've replaced 'import boost.xyz' with this import :interface; // Rest of the file unmodified @@ -706,27 +704,42 @@ import :interface; #include "base64.hpp" ``` -The new `boost_xyz_interface.cppm` should be placed in the public -file set in CMake. +The new `boost_xyz_interface.cppm` should be placed in the `PUBLIC FILE_SET` +in CMake. ## Design decisions -* Why ABI breaking vs other approaches? - * Vs. export using: because it works. export using (TBC: include an example) looks attractive because - it's non-intrusive. But once you try to build enough of your test suite with it, you realize it doesn't - work. This is because of GMF discards: there are a number of corner cases (TBC: link to modules4) - where entities are discarded and change the meaning of the program - * The tuple protocol - * Template specializations - * Vs. non-ABI-breaking: because it's easier for the compiler to diagnose ODR violations. - The cost is not being able to mix include/import. - TBC: expand this argument. -* Why using a static library in CMake -* Why not using the preprocessor to make `.cpp` files module units conditionally. - The most straightforward idea would be to write the following: -* Why can't headers be used directly in cpp files? - If we include one of these headers into several translation units, we will get redefinition - errors. We can't include them in the GMF because they need access to the module private content. +### Why ABI breaking? + + +(TODO: links) + +There are three main approaches to modularizing a library: + +* **`export using`**: wrapping declarations in `export using` + statements looks attractive because it's non-intrusive. However, + it doesn't work in practice. Global module fragment discards cause + corner cases where entities are silently dropped, changing the meaning + of the program. For example, if you try to export `asio::awaitable` with this + approach, `std::coroutine_traits` specializations get discarded, rendering + the type unusable. +* **Non-ABI-breaking**: preserves the ability to mix `#include` and + `import`. The ABI-breaking approach makes it easier for the compiler + to diagnose ODR violations, at the cost of not being able to mix + including and importing. +* **ABI breaking** (our choice): mark entities with an export macro. + Better ODR diagnostics and, in theory, less work for the compiler. + +### Why a static library in CMake? + +Module units are translation units that produce a (usually tiny) +binary containing the module initializer. Using `STATIC` simplifies +deployment and avoids the overhead of shared library machinery for +what is typically a no-op function. + +### Why can't the preprocessor make `.cpp` files into module units? + +The most straightforward idea would be: ```cpp @@ -752,6 +765,13 @@ int boost::xyz::f() { /* ... */ } ``` -This doesn't work because the module declarations need to be the first things to happen -in the translation unit. This limitation probably makes dependency scanning more efficient. +This doesn't work because module declarations must be the first +declarations in the translation unit. This limitation likely exists +to make dependency scanning more efficient. + +### Why can't source-only headers be used directly? +If we include a source-only header in several translation units, we +get redefinition errors. We can't include them in the global module +fragment either, because they need access to the module's private +content. Wrapping them in partition units solves both problems. From 4aa11514bd8c62b22220a036b98813137aee21af Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 21:41:03 +0100 Subject: [PATCH 16/19] some links --- modules.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/modules.md b/modules.md index cfbf663..9590406 100644 --- a/modules.md +++ b/modules.md @@ -31,12 +31,13 @@ Non-goals: ## User interface Users need to consume Boost via CMake, enabling module support by -specifying `-DBOOST_USE_MODULES=1`. Users may either consume Boost -directly using `add_subdirectory`, or build it first, install it, -and using `find_package`. (TODO) +specifying `-DBOOST_USE_MODULES=1`. Boost can be consumed either +directly using `add_subdirectory`, or loaded with `find_package` after +building and installing it. Each Boost library gets its own C++20 module. For example, Boost.Mp11 -can be consumed with `import boost.mp11`. (TODO: exceptions) +can be consumed with `import boost.mp11`. Libraries that export mainly +macros, like Boost.Config, remain as headers. When consuming a Boost distribution built with modules, all public Boost headers are translated into their corresponding `import`, plus @@ -51,10 +52,10 @@ dependencies first. This restriction can probably be lifted, but hasn't been researched yet (and dependency order yields better build-time performance). -The recommended approach is the "ABI breaking" style (TODO: link): mark all public +The recommended approach is the [ABI breaking style](https://clang.llvm.org/docs/StandardCPlusPlusModules.html#abi-breaking-style): mark all public entities with a `BOOST_XYZ_MODULE_EXPORT` macro that expands to `export` when building with modules, then include all public headers -in the module purview. +in the module purview. (TODO: link to rationale) The rest of this section is a step-by-step guide on how to achieve this. @@ -117,7 +118,7 @@ target_link_libraries(boost_xyz Module units are translation units, so when using modules, the library is no longer an `INTERFACE` library. A small binary is generated containing only the module initializer (a function that initializes global variables, -typically a no-op for header-only libraries) (TODO: expand). The updated CMake code: +typically a no-op for header-only libraries) (TODO: link to rationale). The updated CMake code: ```cmake if (BOOST_USE_MODULES) @@ -484,7 +485,7 @@ additional ones described here. Most `.cpp` files need to use or implement private functionality not exported by the module. For this to work, they need to be part of the module. Using the preprocessor doesn't help here (see -[Design decisions](#design-decisions)). +[Design decisions](#design-decisions)). (TODO: update this link to the proper place) We recommend creating a separate file with a different extension for each `.cpp` file. For example, given `utils.cpp`, create `utils.cc`: @@ -573,7 +574,7 @@ Functionality shared by several `.cpp` files is usually placed into header files that live within `src/`. These headers are "source-only": they don't get installed like the ones under `include/`. -To avoid redefinition errors (see [Design decisions](#design-decisions)), +To avoid redefinition errors (see [Design decisions](#design-decisions) TODO: proper link), wrap these headers into module implementation partition units (partitions not marked with `export`). For example, given `base64.hpp`, create `base64.cppm`: @@ -711,27 +712,27 @@ in CMake. ### Why ABI breaking? - -(TODO: links) - There are three main approaches to modularizing a library: -* **`export using`**: wrapping declarations in `export using` +* [`export using`](https://clang.llvm.org/docs/StandardCPlusPlusModules.html#export-using-style): + wrapping declarations in `export using` statements looks attractive because it's non-intrusive. However, it doesn't work in practice. Global module fragment discards cause corner cases where entities are silently dropped, changing the meaning of the program. For example, if you try to export `asio::awaitable` with this approach, `std::coroutine_traits` specializations get discarded, rendering the type unusable. -* **Non-ABI-breaking**: preserves the ability to mix `#include` and +* [Non-ABI-breaking](https://clang.llvm.org/docs/StandardCPlusPlusModules.html#export-extern-c-style): preserves the ability to mix `#include` and `import`. The ABI-breaking approach makes it easier for the compiler to diagnose ODR violations, at the cost of not being able to mix including and importing. -* **ABI breaking** (our choice): mark entities with an export macro. +* [ABI breaking](https://clang.llvm.org/docs/StandardCPlusPlusModules.html#abi-breaking-style) (our choice): mark entities with an export macro. Better ODR diagnostics and, in theory, less work for the compiler. ### Why a static library in CMake? +TODO: add description about initializer symbols + Module units are translation units that produce a (usually tiny) binary containing the module initializer. Using `STATIC` simplifies deployment and avoids the overhead of shared library machinery for From 13ce5d2884de8aef035e568cfd5dd00cbdf87983 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 21:53:52 +0100 Subject: [PATCH 17/19] inlines --- modules.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules.md b/modules.md index 9590406..02c9913 100644 --- a/modules.md +++ b/modules.md @@ -446,6 +446,25 @@ The best approach is to detect misuse and issue an error: Headers like ``, `` and `` use this technique. +### Making member functions inline + +Member functions in non-module code are implicitly inline. +This is no longer true in module code. If you want functions to remain +inline, you need to mark them explicitly: + +```cpp +// +// File: header.hpp +// + +class some_class { +public: + inline some_class() {} + inline int get() { return 42; } +}; + +``` + ### Running the test suite Running a large part of the library's test suite is critical for From a5bf8c8e6120ad6adc9c26eaa39109dd468c93ae Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 21:57:20 +0100 Subject: [PATCH 18/19] proper links --- modules.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules.md b/modules.md index 02c9913..d831538 100644 --- a/modules.md +++ b/modules.md @@ -55,7 +55,7 @@ build-time performance). The recommended approach is the [ABI breaking style](https://clang.llvm.org/docs/StandardCPlusPlusModules.html#abi-breaking-style): mark all public entities with a `BOOST_XYZ_MODULE_EXPORT` macro that expands to `export` when building with modules, then include all public headers -in the module purview. (TODO: link to rationale) +in the module purview (see [Why ABI breaking?](#why-abi-breaking)). The rest of this section is a step-by-step guide on how to achieve this. @@ -118,7 +118,7 @@ target_link_libraries(boost_xyz Module units are translation units, so when using modules, the library is no longer an `INTERFACE` library. A small binary is generated containing only the module initializer (a function that initializes global variables, -typically a no-op for header-only libraries) (TODO: link to rationale). The updated CMake code: +typically a no-op for header-only libraries) (see [Why a static library in CMake?](#why-a-static-library-in-cmake)). The updated CMake code: ```cmake if (BOOST_USE_MODULES) @@ -504,7 +504,7 @@ additional ones described here. Most `.cpp` files need to use or implement private functionality not exported by the module. For this to work, they need to be part of the module. Using the preprocessor doesn't help here (see -[Design decisions](#design-decisions)). (TODO: update this link to the proper place) +[Why can't the preprocessor make `.cpp` files into module units?](#why-cant-the-preprocessor-make-cpp-files-into-module-units)). We recommend creating a separate file with a different extension for each `.cpp` file. For example, given `utils.cpp`, create `utils.cc`: @@ -593,7 +593,7 @@ Functionality shared by several `.cpp` files is usually placed into header files that live within `src/`. These headers are "source-only": they don't get installed like the ones under `include/`. -To avoid redefinition errors (see [Design decisions](#design-decisions) TODO: proper link), +To avoid redefinition errors (see [Why can't source-only headers be used directly?](#why-cant-source-only-headers-be-used-directly)), wrap these headers into module implementation partition units (partitions not marked with `export`). For example, given `base64.hpp`, create `base64.cppm`: From e29bc2a5bd3493d353bf597610d4b32b5a11d0dc Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 7 Mar 2026 22:04:59 +0100 Subject: [PATCH 19/19] expand on initializers --- modules.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/modules.md b/modules.md index d831538..da78980 100644 --- a/modules.md +++ b/modules.md @@ -117,8 +117,8 @@ target_link_libraries(boost_xyz Module units are translation units, so when using modules, the library is no longer an `INTERFACE` library. A small binary is generated containing only -the module initializer (a function that initializes global variables, -typically a no-op for header-only libraries) (see [Why a static library in CMake?](#why-a-static-library-in-cmake)). The updated CMake code: +the module initializer (see [Why a static library in CMake?](#why-a-static-library-in-cmake) for more info). +The updated CMake code: ```cmake if (BOOST_USE_MODULES) @@ -750,13 +750,17 @@ There are three main approaches to modularizing a library: ### Why a static library in CMake? -TODO: add description about initializer symbols - Module units are translation units that produce a (usually tiny) -binary containing the module initializer. Using `STATIC` simplifies -deployment and avoids the overhead of shared library machinery for +binary containing the module initializer. This function initializes any +global variables declared in the module. + +Using `STATIC` simplifies deployment and avoids the overhead of shared library machinery for what is typically a no-op function. +Libraries may contain symbols if you forgot to add `inline` specifiers +to your class members. It's advised to check the generated binaries +to fix these cases. + ### Why can't the preprocessor make `.cpp` files into module units? The most straightforward idea would be: