Skip to content

C++20 module support#105

Draft
anarthal wants to merge 21 commits intoboostorg:developfrom
anarthal:feature/cxx20-modules
Draft

C++20 module support#105
anarthal wants to merge 21 commits intoboostorg:developfrom
anarthal:feature/cxx20-modules

Conversation

@anarthal
Copy link
Contributor

@anarthal anarthal commented Mar 7, 2026

Adds support to install CXX_MODULES FILE_SETs
Adds a document with the recommended modularization stratey

@anarthal
Copy link
Contributor Author

anarthal commented Mar 7, 2026

Rendered document talking about the modularization strategy: https://github.com/anarthal/boost-cmake/blob/feature/cxx20-modules/modules.md

@anarthal
Copy link
Contributor Author

anarthal commented Mar 8, 2026

@DanielaE @ClausKlein this is similar to what you've been doing in https://github.com/ClausKlein/asio/ but for the rest of Boost. It'd be helpful if you could have a look to the proposed strategy and some of the PRs and provide some feedback. Thanks.

@anarthal
Copy link
Contributor Author

anarthal commented Mar 8, 2026

@ChuanqiXu9 FYI, your feedback also welcome

@pdimov
Copy link
Member

pdimov commented Mar 8, 2026

I don't think I like putting the stdlib forwarding headers in Config. I'd rather include boost/std/utility.hpp and have a separate repo boostorg/std for these.

Related, at the moment we have a single macro for "use import std" and "create boost.foo modules". I think that using import std can be desirable even if Boost isn't consumed as modules; whether this use needs to be automatically detected and enabled without explicitly requested via a macro is an interesting question.

Why do we need the "in module purview" check?

@anarthal
Copy link
Contributor Author

anarthal commented Mar 8, 2026

I don't think I like putting the stdlib forwarding headers in Config. I'd rather include boost/std/utility.hpp and have a separate repo boostorg/std for these.

I can create a separate repo.

Why do we need the "in module purview" check?

Because imports in a module must follow the module unit declaration. This is illegal:

module x;
import a;
int i = 42;
import b; // should be before the int decl

This constraint does not apply to non-module code.

See this for the rationale: https://github.com/anarthal/boost-cmake/blob/feature/cxx20-modules/modules.md#creating-the-compatibility-headers

Related, at the moment we have a single macro for "use import std" and "create boost.foo modules". I think that using import std can be desirable even if Boost isn't consumed as modules; whether this use needs to be automatically detected and enabled without explicitly requested via a macro is an interesting question.

This has edges. #include <cmath> being replaced by import std works 95% of the time. Unless you're using HUGE_VAL. Then you need to include the header. But this needs to happen in the GMF if you're including the header in the purview.

Something I could potentially do is:

  • Have <boost/std/cmath.hpp> and <boost/std/cmath_macros.hpp> for each of the standard headers that document macros.
  • cmath.hpp expands to import std in GMF/non-module code, to nothing in the purview.
  • cmath_macros.hpp expands to #include <cmath> in GMF/non-module code, to nothing in the purview.

I can also do the other way around (i.e. cmath_functions.hpp).

Note however that this errors on I'd say the 3 compilers:

import std;
#include <cmath>

Although it should be legal, but things are what they are.

@pdimov
Copy link
Member

pdimov commented Mar 8, 2026

There's not much gain from not including <cmath>, so we might just leave it a header.

But this same reasoning also applies to the feature macros. It seems to me that boost/std/foo.hpp should actually be

import std;
#include <version>

rather than just import std;.

@anarthal
Copy link
Contributor Author

anarthal commented Mar 8, 2026

Is it not? Many functions there are supposed to be constexpr so I assume there's some non-trivial things going on in this header. I haven't measured though.

I agree that this is a special case though. cerrno, cfloat, cassert and friends are probably trivial enough. Not sure about cstdio though.

The version addition makes sense.

Copy link

@ChuanqiXu9 ChuanqiXu9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent doc!

A few points:


For converting the library, I suggest to define a macro:

#ifdef BOOST_IN_MODULE_PURVIEW
#define BOOST_INLINE
#else
#define BOOST_INLINE inline
#endif

Then we can put this in front of boost's inline functions.

The less inline code in the module purview, the compile time improvements and code size improvements are bigger for users.

Although I already see you mentioned you worry about distribution problems, but given you have to have an object binary in the end, I feel it might be solvable problem. Especially they are users who build every thing from source.


As a user, I feel better to see a section how can we workaround when the official support is not landed. e.g, in this doc, we can say the users can wrap boost module itself if they want to use it. For example, I have: https://github.com/ChuanqiXu9/beast and it seems like there are already some use examples: https://github.com/search?q=%22import+boost%3B%22+language%3AC%2B%2B&type=code . And in this doc, we can say, boost didn't choose the simple way to wrap boost module since boost has a higher bar to ship features.


// 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can use #include "header.h"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule applies recursively. Even if you use #include "boost/xyz.hpp", you will get warnings for all the other headers included by boost/xyz.hpp, which use angle-brackets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I've found what I think is a problem in clang-scan-deps - while clang respects my warning suppression, clang-scan-deps does not and emits a tons of warnings during scanning. Do you want me to provide a minimal reproducible bug report for this? Or is it a known issue?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't heard it. I think a bug report may be worthy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes:

* We use `STATIC` (rather than letting the user choose) because the
binary is expected to contain almost no code. This simplifies

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

binary is expected to contain almost no code

This may not be true with ABI Breaking style as you put headers in the module purview, the in-class defined functions are not inlined right now. Maybe you can double check this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend in a later paragraph to stick an explicit inline in front of member functions.

I think allowing SHARED here is opening a can of worms. You now need to start worrying about __declspec(dllexport) in Windows and symbol visibility under Linux. Also, I don't think that simple getters should end up behind a DLL boundary. At the very minimum, authors need to be aware of this and choose to enable this explicitly.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. As I am not boost's dev, I won't push on it. Just my 2 cents. My opinion is, generally we don't have to put an inline in front of member functions. And as doc, i feel it is better to say: if you want to provide it as shared, be careful about the symbol exported in shared libraries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a comment explaining the tradeoffs then. I think it may be worth not inlining some of the functions. Author's choice seems fine to me.


Some notes:

* We use `STATIC` (rather than letting the user choose) because the

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel shared library may be fine too. Is it a problem for you?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the above point.

Comment on lines +451 to +453
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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in most cases we don't have to do so?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For correctness, no. For performance, I think this is still to be determined, some compilers will factor this into their decisions on inlining. I'm fairly sure that I already experienced one case where I lost some significant performance because an extremely trivial function ended up not being inlined. Of course it depends on many factors including LTCG, but anyway I think it's good to be aware of the difference.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For correctness, no. For performance, I think this is still to be determined, some compilers will factor this into their decisions on inlining. I'm fairly sure that I already experienced one case where I lost some significant performance because an extremely trivial function ended up not being inlined. Of course it depends on many factors including LTCG, but anyway I think it's good to be aware of the difference.

Yeah, it is better to mention it explicitly. For runtime performance, my experience is, we should use LTO/ThinLTO with modules to get best performance. I don't feel this is extra requirement to me. Since previously we already need LTO/ThinLTO to get best performance.

@anarthal
Copy link
Contributor Author

anarthal commented Mar 9, 2026

As a user, I feel better to see a section how can we workaround when the official support is not landed. e.g, in this doc, we can say the users can wrap boost module itself if they want to use it. For example, I have: https://github.com/ChuanqiXu9/beast and it seems like there are already some use examples: https://github.com/search?q=%22import+boost%3B%22+language%3AC%2B%2B&type=code . And in this doc, we can say, boost didn't choose the simple way to wrap boost module since boost has a higher bar to ship features.

Does your module implementation work well with clang-22, which enables global module fragment discards by default? I've done an experiment in a pet project which uses Beast (anarthal/servertech-chat#77) and the results were frankly bad. Exporting asio::awaitable required exporting a dummy coroutine function to prevent std::coroutine_traits specializations from being discarded. I was completely unable to make websockets work at all with export using - I had to include the file in the GMF. I still need to measure compile times, but my feeling is not good.

My takeaway from this experiment is "export using does not work in the general case". It might work for std or for libraries only exporting functions, but it seems to fail for heavily templated code. So I'm not very keen on recommending it at this point, unless I'm missing something.

@ChuanqiXu9
Copy link

Does your module implementation work well with clang-22, which enables global module fragment discards by default? I've done an experiment in a pet project which uses Beast (anarthal/servertech-chat#77) and the results were frankly bad. Exporting asio::awaitable required exporting a dummy coroutine function to prevent std::coroutine_traits specializations from being discarded. I was completely unable to make websockets work at all with export using - I had to include the file in the GMF. I still need to measure compile times, but my feeling is not good.

I am using latest trunk. I didn't met your problem. For coroutines, I use my library: https://github.com/alibaba/async_simple. And I didn't find problems yet.

For the thread itself, I think you made the right choice. Although it requires harder work, the ABI breaking style may be the best in the long term.

My takeaway from this experiment is "export using does not work in the general case". It might work for std or for libraries only exporting functions, but it seems to fail for heavily templated code. So I'm not very keen on recommending it at this point, unless I'm missing something.

I didn't met this. Techniquelly, the compiler is allowed to elide things which it think unnecessary ("unnecessary" is almost implementation defined here.) And if we want something to keep unelided, we can using it without exporting it. The exporting using style have problems, e.g, it can't using friend entities. And most existing users of C++20 modules use exporting using style more or less.

Copy link

@ClausKlein ClausKlein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some first notes

a typical Boost `CMakeLists.txt`:

```cmake
cmake_minimum_required(VERSION 3.5...3.31)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import std; is starts really to be usable with cmake v4.2, better witch current v4.3-rec2.
But it seems still experimanteal at the moment!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think we will merge this once CMake makes it stable (4.4 I guess?). The minimum can't be bumped to that high because we intend to keep supporting headers and older systems. I should update the higher version range though.

Comment on lines +133 to +136
else()
add_library(boost_xyz INTERFACE)
set(__scope INTERFACE)
endif()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not always add an header only interface lib too where it is possible?

This would allow to use a FILE_SET HEADERS and enable VERIFY_INTERFACE_HEADER_SETS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would that this interface library be used? If building with BOOST_USE_MODULES, headers expand to imports so using it directly would lead to missing modules.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dit it on my asio feature/module branch.

The problem would be the installation: you need to install both libraries, and one is optional!

I have written a generic install cmake module for the Beman project to handle this.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants