Skip to content

Add optional C++20 module interface (import boost.program_options) - #161

Open
soroshsabz wants to merge 17 commits into
boostorg:developfrom
soroshsabz:feature/cxx20-modules
Open

soroshsabz wants to merge 17 commits into
boostorg:developfrom
soroshsabz:feature/cxx20-modules

Conversation

@soroshsabz

@soroshsabz soroshsabz commented Oct 21, 2026 •

Copy link
Copy Markdown

ITNOA

Hi,

This PR fixes #160.

Summary

Adds an optional C++20 module interface for the library, so that

import boost.program_options;

can be used instead of #include <boost/program_options.hpp>, while the boost::program_options namespace and all existing code keep working unchanged.

Design

src/cppm/boost/program_options.cppm — no per-name using-declarations, no new .cpp files, no header modifications:

  • The interface preloads the library's dependencies in the global module fragment and includes the umbrella header <boost/program_options.hpp> in the module purview inside an export { ... } block, exporting its declarations without touching any header.
  • Each existing src/*.cpp gains a leading BOOST_PROGRAM_OPTIONS_MODULE_UNIT conditional that, in the module build, makes the file a module implementation unit of boost.program_options (GMF preloads, module boost.program_options;, and the library's own include guards predefined so its headers are no-ops — their declarations come from the imported interface). In classic builds the same files compile unchanged. This provides the module-attached definitions that match the interface (required because MSVC mangles module-attached functions with a module qualifier, so the classic library cannot provide them — verified with fresh builds).
  • The CMake module target compiles the same src/*.cpp with the define plus the interface, scanning them so they build against the BMI.
  • 'import std;' is used in addition to textual includes when the toolchain provides the std module; on MSVC the import must come after textual std includes (verified: reverse order gives C2011 redefinitions).
  • boost::typeindex comparison operators remain re-exported: the preloaded boost::any_cast is instantiated in importing TUs and finds them by ADL only if exported (MSVC C2679 otherwise, verified three times).
  • Macros (e.g. BOOST_PROGRAM_OPTIONS_VERSION) are not exported by C++20 modules; code that needs them keeps including the classic headers.

Build system

  • CMake: the module is opt-in via BOOST_PROGRAM_OPTIONS_BUILD_MODULES=ON and produces an additional Boost::program_options_modules target that links the classic library. Requires CMake 3.28+ with the Ninja generator, or CMake 3.30+ with the Visual Studio generator, and a compiler with C++20 modules support (MSVC, GCC 15+, Clang 16+). The interface source is installed next to the headers when the option is enabled.
  • B2: not touched. b2 does not support building C++20 module interfaces yet; this can be revisited once the build system grows module support.

Tests

test/module_test.cpp consumes only import boost.program_options; (no Boost header is included) and exercises the exported surface end to end: command line parsing with all option kinds, positional options, unregistered options, defaulted and implicit values, config files, the wide-character interface (wcommand_line_parser, which uses the basic_parsed_options<wchar_t> specialization), the exception hierarchy, operator<< (found via ADL), and utilities (strip_prefixes, split_unix).

It runs as a plain add_test (no Boost.Test dependency) whenever the module is enabled.

Validated locally with MSVC 19.51 (both Ninja and the Visual Studio generator, Debug and Release); all pre-existing tests still pass with the change.

CI

New jobs build and run the module test with MSVC (VS generator, Debug + Release), GCC 15 and Clang 21 (Ninja, ubuntu:26.04 containers).

Documentation

README.md gains a "C++20 module support" section, and a new doc/modules.xml page documents usage, build requirements and limitations.

Implements boostorg#160.

Adds a C++20 module interface for the library, so that

    import boost.program_options;

can be used instead of including <boost/program_options.hpp>, with the
boost::program_options namespace unchanged.

The interface (src/cppm/boost/program_options.cppm) is a thin wrapper:
the classic umbrella header is included in the global module fragment
and its public declarations are re-exported with exported
using-declarations. No header is modified, the compiled library is
shared as is, and the classic header-based interface remains the
default, so the change is strictly additive.

- The module is built only when CMake is configured with
  BOOST_PROGRAM_OPTIONS_BUILD_MODULES=ON (CMake 3.28+ with Ninja,
  3.30+ with the Visual Studio generator), producing the additional
  Boost::program_options_modules target which links the classic
  library.
- test/module_test.cpp verifies the exported interface without
  including any Boost header: command line parsing with all option
  kinds, positional and unregistered options, defaulted and implicit
  values, config files, the wide-character interface, exceptions, and
  utilities.
- CI jobs build and run the module test with MSVC (Visual Studio
  generator), GCC 15 and Clang 21 (Ninja).
- Documented in README.md and doc/modules.xml.

Macros (e.g. BOOST_PROGRAM_OPTIONS_VERSION) are not exported by the
module; code needing them keeps including the classic headers.
Place the C++20 module interface in a root-level modules/ directory,
naming it boost_program_options.cppm, following the layout used by
other Boost libraries (e.g. Boost.Any). No functional changes.
Adds a minimal, compilable example of importing boost.program_options
in modules/usage_sample.cpp, following the layout of other Boost
libraries (e.g. Boost.Any), and references it from the documentation.

Also expands the comment in the module interface explaining why the
exported using-declaration list is needed: unlike libraries whose
public headers carry export macros and are included in the module
purview, this module keeps every header untouched and therefore must
re-export each public name explicitly from the global module fragment.
@soroshsabz

Copy link
Copy Markdown
Author

@vprus please review this PR

thanks :)

Replace the exported using-declaration wrapper with the design used by
other Boost libraries that ship a module (Boost.Any, Boost.PFR):

- <boost/program_options/config.hpp> defines
  BOOST_PROGRAM_OPTIONS_BEGIN/END_MODULE_EXPORT, which expand to
  'export { ... }' while the module interface unit is compiled
  (BOOST_PROGRAM_OPTIONS_INTERFACE_UNIT) and to nothing otherwise. It
  also turns any library header into 'import boost.program_options;'
  when BOOST_USE_MODULES is defined, as Boost.PFR does.

- The public declarations in every umbrella-reachable header are
  wrapped in those export blocks; header builds are unchanged.

- modules/boost_program_options.cppm now preloads the dependency and
  standard headers used by the public headers in the global module
  fragment (so the purview includes are no-ops for them and nothing
  foreign gets attached to the module) and includes the umbrella
  header in the purview. The long list of using-declarations is gone.

Unlike Boost.Any/PFR, this library is compiled and depends on Boost
libraries that are not modularized yet (any, function, smart_ptr,
lexical_cast), which is why their headers are preloaded in the global
module fragment rather than imported.

cmdline.hpp now includes the library config header so the export
macros are always defined. All 15 CMake tests pass, in both header
mode and module mode.
Boost.Any and Boost.PFR guard their standard library consumption with
an optional 'import std;' branch. The equivalent branch was tried here
and does not work for this library yet: its public headers depend on
Boost libraries that are not modularized (Function, LexicalCast,
SmartPtr, Any), those headers textually include the standard library,
and MSVC rejects that mix with redefinition errors (std::align_val_t,
std::byte, ...).

Boost.Any only gets away with it because its remaining textual
includes (assert, config, throw_exception) are macro-only headers.

Revert to unconditional textual preloads and document in
modules/boost_program_options.cppm and CMakeLists.txt why, and that
the guarded 'import std;' branch can be added once the dependencies
are modularized.
… re-export

Removing the boost::typeindex operator re-export to test whether
boost.type_index shipping its own module makes it redundant breaks the
build (MSVC C2679 in boost/any.hpp: type_index == type_index is not
found by ADL in importing translation units). The operators are still
needed because boost.type_index is not imported here: its module is
not built without BOOST_USE_MODULES infrastructure, and
<boost/type_index.hpp> is preloaded textually in the global module
fragment. Document this in the module interface; the block can be
dropped once dependencies are modularized and imported.

The experiment also exposed a latent brace-scoping bug: in
detail/value_semantic.hpp the export block was opened outside the
namespace boost { namespace program_options { block but closed inside
it, so in module builds the 'using namespace validators;' directive
landed in the wrong scope and the validate/value/wvalue definitions
were attached incorrectly. The export blocks are now properly nested
inside the namespace, split around the using-directive.
Investigation of the import std failure found the actual root causes of
several problems:

1. On MSVC, a textual standard header parsed after 'import std;'
   clashes with the declarations provided by the std module
   (C2011 redefinitions); textual includes must come first. The import
   is now placed after the global module fragment preloads, and
   'import std;' is enabled when CMake provides the std module
   (CMAKE_CXX_MODULE_STD + CMAKE_EXPERIMENTAL_CXX_IMPORT_STD), like in
   Boost.Any. Boost.Any gets away with fewer preloads because its
   remaining textual dependencies are macro-only headers.

2. More importantly, declarations in a module purview attach to the
   module, and MSVC mangles their out-of-line definitions with a
   module qualifier, so an interface built the export-block way cannot
   link against the classic, non-modular library (unresolved externals
   like ??0options_description@...Z::<!boost.program_options>). A
   stale ninja dyndep graph had masked this in earlier local builds;
   fresh builds fail with 43 unresolved externals.

   Unlike Boost.Any/PFR, this library is compiled, so the module now
   follows the compiled-library recipe: modules/impl/*.cc are module
   implementation units that compile src/*.cpp into the module, giving
   the definitions the same attachment as the exported declarations.
   The boost_program_options_modules CMake target is self-contained
   (it no longer links the classic library) and scans its sources so
   the implementation units are compiled against the interface BMI.

   In implementation units the library headers are no-ops (declarations
   come from the imported primary interface; re-including them textually
   would redeclare the module's own entities), so every header covered
   by the interface is guarded with BOOST_PROGRAM_OPTIONS_MODULE_IMPL_UNIT.
   Internal headers used by the sources (detail/config_file.hpp,
   environment_iterator.hpp) are included once, unexported, in the
   interface instead. The shared std/Boost dependencies are preloaded
   textually in modules/gmf_preload.hpp, shared by all module units.

The module test consumes import std when the module was built with it.
Validated with fresh build directories: 15/15 tests with textual
preloads, and the module test also passes built with import std (C++23
+ std module).
…sign

Re-verified that the boost::typeindex operator re-export is still
required with the module implementation unit architecture: removing it
fails the module test with MSVC C2679 in boost/any.hpp (any_cast
compares type_index objects by ADL in importing translation units).
The operators belong to a preloaded, global-module dependency header,
so neither the export blocks nor the implementation units of this
module cover them. Documented; the block stays until the dependencies
are modularized and imported.
@soroshsabz soroshsabz changed the title Add optional C++20 module interface (import boost.program_options) [WIP]: Add optional C++20 module interface (import boost.program_options) Oct 22, 2026
@soroshsabz
soroshsabz marked this pull request as draft October 22, 2026 10:50
Per review feedback, revert the export-block/implementation-unit
architecture and keep the library headers completely untouched, with all
module-related files under src/:

- The module interface is again a thin wrapper
  (src/cppm/boost/program_options.cppm): the classic umbrella header is
  included in the global module fragment and the public declarations
  are re-exported with exported using-declarations. No header is
  modified, declarations stay attached to the global module, and the
  definitions in the classic compiled library link directly, so no
  module implementation units and no global-module-fragment preload
  list are needed (modules/impl/*.cc and modules/gmf_preload.hpp
  removed).

- The header export-block style used by Boost.Any/Boost.PFR requires,
  for a compiled library, module implementation units (MSVC mangles
  module-attached functions with a module qualifier, so the classic
  library cannot provide their definitions) and modularized
  dependencies. That migration can be revisited once the Boost
  dependencies are modularized; documented in the interface.

- modules/usage_sample.cpp moves to src/cppm/usage_sample.cpp; CMake,
  README and doc/modules.xml updated accordingly.

Validated in fresh build directories: all 15 tests pass, and the module
test also passes built with 'import std;' (C++23 + std module).
Per review feedback, drop the exported using-declaration list from the
module interface and avoid creating any extra .cpp files:

- The interface (src/cppm/boost/program_options.cppm) now preloads the
  library's dependencies in the global module fragment and includes the
  umbrella header in the module purview inside an 'export { ... }'
  block, so the public declarations are exported without modifying any
  header and without per-name using-declarations.

- Each src/*.cpp gains a leading BOOST_PROGRAM_OPTIONS_MODULE_UNIT
  conditional that, in the module build, turns the file into a module
  implementation unit of boost.program_options (global module fragment
  preloads, 'module boost.program_options;', and the library's own
  include guards predefined so its headers are no-ops there - their
  declarations come from the imported interface). In classic builds
  the files compile unchanged. This provides the module-attached
  definitions matching the interface, which the classic library cannot
  (some compilers mangle module-attached functions differently).

- The module CMake target compiles the same src/*.cpp with
  BOOST_PROGRAM_OPTIONS_MODULE_UNIT and scans them so they are built
  against the interface BMI; it is otherwise like the classic target.

The boost::typeindex operator export remains in the interface: the
preloaded boost::any_cast is instantiated in importing translation units
and finds those operators by ADL only if the module exports them (MSVC
error C2679 otherwise).

Validated in fresh build directories: all 15 tests pass (classic build
unchanged; module test green), and the module test also passes built
with 'import std;' (C++23 + std module).
Each BOOST_PROGRAM_OPTIONS_MODULE_UNIT prelude now preloads only the
headers that file itself includes and uses (its own includes plus what
its body uses that classically arrived transitively through the
library headers, e.g. <set> in value_semantic.cpp, <algorithm> and
iterator_facade in parsers.cpp, std::size_t in winmain.cpp), and
predefines only the include guards of the library headers it includes,
instead of a uniform superset. The interface preload was likewise
reduced to what the umbrella header and the internal headers use.

About import std: the textual standard includes in the global module
fragment are load-bearing in both modes. import std does not define
include guards, so the (unmodifiable) library headers textually
including <string> etc. in the module purview would either attach the
standard library to this module or, on MSVC, parse after the std
module declarations and fail with C2011 redefinitions (verified with a
controlled experiment; the order textual-then-import is the one that
works). Libraries whose headers are converted to compatibility headers
(Boost.Any) can drop the textual includes; that requires header
changes, which this PR deliberately avoids.
…ncludes

Every standard-header include of the module units (the interface and
the BOOST_PROGRAM_OPTIONS_MODULE_UNIT preludes in src/*.cpp) now goes
through a local compatibility shim in src/cppm/std/, following the
pattern of the boost/config/std headers from boostorg/config PR #535
(Adds support for C++20 modules, not merged yet):

    #ifdef BOOST_USE_MODULES
    #ifndef BOOST_IN_MODULE_PURVIEW
    import std;
    #endif
    #else
    #include <string>
    #endif

Today the shims compile to the plain includes (no behavior change);
once PR #535 is merged and the future modules mode (BOOST_USE_MODULES)
is used, they provide import std instead of textual includes inside
module units. Each shim carries a TODO comment to replace it with the
boost/config/std counterpart at that point; the README and the
documentation mention the PR as well.

Validated in fresh build directories: 15/15 tests, and the module test
with import std.
…ST_USE_MODULES

Every standard-header inclusion in the library's headers and sources is
now wrapped in one compatibility guard per include block, following the
pattern of the boost/config/std headers from boostorg/config PR #535
(Adds support for C++20 modules, not merged yet):

    #ifdef BOOST_USE_MODULES
    #ifndef BOOST_IN_MODULE_PURVIEW
    import std;
    #endif
    #else
    #include <...>
    #endif

The guards are inactive today (BOOST_USE_MODULES is not defined), so
behavior is unchanged; each carries a TODO to switch to
<boost/config/std/...> when the PR is merged. In module units the
guarded std block sits after the textual dependency preloads, keeping
the MSVC-safe textual-before-import order, and the units define
BOOST_IN_MODULE_PURVIEW after their module declaration, per the
convention of that PR.

BOOST_USE_MODULES can now be enabled from CMake: the option activates
the module build, defines the macro for the module target, and requires
the toolchain std module (CMAKE_CXX_MODULE_STD plus a standard in
CMAKE_CXX_COMPILER_IMPORT_STD; fatal error otherwise). Full activation
of the ecosystem mode additionally requires modularized dependencies:
under this flag, already-converted dependencies such as Boost.TypeIndex
switch their headers to importing their own modules, superseding the
textual preloads used here until the PR #535 ecosystem lands. Noted in
CMakeLists.txt, README.md and doc/modules.xml.

Validated in fresh build directories: all 15 tests (classic build
unchanged) and the module test, both with textual preloads and with
import std.
Place boost_program_options.cppm and usage_sample.cpp in a root-level
modules/ directory, matching the layout of the modularized Boost
libraries (Boost.Any, Boost.PFR, Boost.TypeIndex). All references
(CMake file set and install rule, comments in the compatibility
guards, README, documentation) are updated accordingly.
…c code

Restructure the BOOST_PROGRAM_OPTIONS_MODULE_UNIT blocks in src/*.cpp:
in module builds the classic include section of each file is now skipped
entirely (#ifndef BOOST_PROGRAM_OPTIONS_MODULE_UNIT), so the module
prelude no longer needs to mirror it through predefined include guards,
and every header appears exactly once per compilation:

- The prelude preloads only the non-program_options headers the classic
  code below includes, plus the few the classic build only got
  transitively through the library's own headers (e.g. <set> for
  value_semantic.cpp, <algorithm> and iterator_facade for parsers.cpp,
  <cstddef>/<string>/<vector> for winmain.cpp). Dead-code extras are
  gone (e.g. <sstream> in config_file.cpp, which is only used by the
  #if 0 block, and the blanket <cstddef>).
- The predefined include-guard defines are removed together with the
  no-op mechanism they served.
- The classic include sections keep their original content and order
  (convert.cpp's std block moved after the library includes so the
  skip wraps the whole section); utf8_codecvt_facet.cpp keeps its
  macro block and .ipp include active in module builds.

Validated in fresh build directories: all 15 tests (classic build
unchanged) and the module test, both with textual preloads and with
import std.
… guards

Restructure src/*.cpp to eliminate the duplicated include lists: in
module builds, the global module fragment is now formed in place around
the file's own include section instead of a parallel prelude list.

Each source now shows its original includes at the original position,
plus three small guards:

- 'module;' opens the global module fragment at the top (module build
  only); the textual includes below attach to the global module.
- The library's own declaration headers are skipped in module builds
  (#ifndef BOOST_PROGRAM_OPTIONS_MODULE_UNIT): the imported primary
  interface provides them.
- 'module boost.program_options;' closes the fragment before the code.

The few headers whose declarations the classic build only gets
transitively through the skipped library headers are included in a
module-only block (e.g. <algorithm> and iterator_facade for parsers.cpp,
<string> and throw_exception for value_semantic.cpp, config.hpp for
split.cpp and winmain.cpp, <cstddef>/<string>/<vector> for winmain.cpp),
placed before the standard-library guard so that textual includes come
before 'import std;' (MSVC requirement).

The standard-library guard in the sources takes the 'import std;'
branch both in the ecosystem mode (BOOST_USE_MODULES) and when the
module was built with 'import std;'
(BOOST_PROGRAM_OPTIONS_USE_STD_MODULE). Files whose code calls assert()
keep <cassert> textual before the guard: 'assert' is a macro, and
import std does not provide macros.

Validated in fresh build directories: all 15 tests (classic build
unchanged), and the module test with textual preloads and with
import std.
convert.cpp originally included the standard headers before the Boost
dependencies; the restructuring had moved them below. Restore the
original order: the standard-library guard now sits at the headers'
original position with the exact boostorg/config#535 pattern
(BOOST_USE_MODULES branch).

Because the textual Boost dependency includes follow it, the
BOOST_PROGRAM_OPTIONS_USE_STD_MODULE 'import std;' for this file moves
into the module closing block (after all textual includes, before the
module declaration): MSVC reconciles textual includes followed by an
explicit import, but rejects an import followed by textual includes.

Validated in fresh build directories: all 15 tests (classic build
unchanged), and the module test with textual preloads and with
import std.
@soroshsabz soroshsabz changed the title [WIP]: Add optional C++20 module interface (import boost.program_options) Add optional C++20 module interface (import boost.program_options) Oct 23, 2026
@soroshsabz
soroshsabz marked this pull request as ready for review October 23, 2026 12:24
@vprus

vprus commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Hi,

thanks for the PR; I'll try to take a look this week. Couple of initial questions:

First, was any AI used to prepare it? If so, to what degree and which model(s)

Second, as you say, this design changes that ABI/mangling and requires a different library

  • Why don't include the main header in the global module, and then export symbols in the module purview, thereby, I think, preserving ABI?
  • I see add_library(boost_program_options_modules, which seems to me like creating a new library that does not follow all the standard Boost library naming scheme - no toolset, no version, or debug/release. Is this intended?
  • In fact, those seems like Boost-wide questions, is there an established consensus how this should be done for compiled libraries, if so can you give a link to the discussion/conclusion?

Third, this is a large PR, for a library that is tiny. Are we using it as some some of playground for modules? Or do we expect any user-visible benefit?

@anarthal

Copy link
Copy Markdown

This is what we currently have for C++20 modules in Boost. Not a lot yet, and I probably need to update some notes there. I'm waiting for CMake to officially support import std (it's been in experimental waaaaaaay longer than I'd have liked) before going forward with it.

I've only had a quick glance, but some things to at least check:

  • To which point does starting a module unit with an #ifdef directive works? I got the impression that that's illegal because it hurts scanning performance, and recent compilers reject it (older clangs used to accept it).
  • To which point does mixing #include <some_std_header> and import std work? From the standard perspective, it should work, but I've found that that's not the case for many real implementations.

It'd be useful to me if @soroshsabz could have a look at the document I pointed out and let me know if the approach there seems to be useful, and provide any feedback.

I've applied the approach to some Boost libraries (I've got forks with feature/cxx20-modules branches on them). They may be a bit behind current develop, but you might find these useful.

Cheers,
Ruben.

@soroshsabz

Copy link
Copy Markdown
Author

@vprus My harness is opencode and my model is GLM 5.3

@soroshsabz

Copy link
Copy Markdown
Author

Hi Vladimir,

thanks for taking a look. (AI answer above: the harness is opencode, model GLM 5.3; I directed the work and reviewed/validated every change.) Answers to your questions:

1. ABI: why not include the main header in the global module fragment and export in the purview?

You're right, and that is exactly where this PR started (see modules/boost_program_options.cppm in the first commit, 84536c9): the umbrella header included in the global module fragment, and exported using-declarations in the purview. All declarations stay attached to the global module, so the classic boost_program_options library links directly and the ABI is unchanged — no extra library, no recompiled sources. That variant is fully validated here (all 15 tests + a module consumer test, built both with textual preloads and with import std;). The only caveat found on MSVC: templates from preloaded dependencies (boost::any_cast, reached via variable_value::as<T>()) look up boost::typeindex::operator== by ADL in importing TUs, and MSVC does not find non-exported global-module operators there (C2679), so the interface must re-export those two operators explicitly (a small export using block; verified necessary three times by removing it).

The current revision instead follows the ABI-breaking style recommended by the Boost C++20 modules effort (see @anarthal's comment and the design notes at anarthal/boost-cmake modules.md): declarations in the module purview attach to the module, and definitions come from module implementation units — which is why the sources are compiled a second time and the extra library exists.

Both designs work; they trade exactly what you'd expect (ABI preservation and a minimal diff vs. the direction the Boost-wide proposal recommends). Given your points below, I'm happy to rebase onto the GMF + export using version — it reduces the change to one new file under modules/, a small opt-in CMake block, one test and docs. Please tell me which you prefer.

2. boost_program_options_modules naming.

Correct. In the current (ABI-breaking) revision it is a separate STATIC library, built only with -DBOOST_PROGRAM_OPTIONS_BUILD_MODULES=ON (CMake opt-in; b2 never builds it), and it does not follow the usual toolset/version/debug-release naming. In the GMF + export using revision this library disappears entirely — the module interface is compiled together with the classic library and inherits the standard naming for free. Another argument for the simpler design.

3. Boost-wide consensus.

Not yet, as Ruben says below. The current working documents are the design notes at https://github.com/anarthal/boost-cmake/blob/feature/cxx20-modules/modules.md and boostorg/config#535; nothing is merged, and Ruben is waiting for CMake import std to leave experimental before moving forward. This PR aligns with those patterns where they apply to a compiled library with not-yet-modularized dependencies, and documents the deviations (my reply to Ruben below lists them, including two MSVC behaviors that may be useful to that effort).

4. Size vs. benefit.

Fair point. The user-visible benefit is opt-in: consumers compiling with C++20 can import boost.program_options; for faster compiles and less macro pollution; header users see no change at all. The current diff is large mostly because it carries both the module infrastructure and the documentation of everything above; the simpler design is roughly one interface file + CMake opt-in + test + docs.

@soroshsabz

Copy link
Copy Markdown
Author

Hi Ruben,

thanks for the pointers. I read modules.md and applied what applies to a compiled library whose dependencies are not modularized yet; the result is in this PR. Answers to your two questions, with MSVC 19.51 (VS 2026, CMake 4.3 + Ninja) findings:

Module units starting with #ifdef: it works in this setup. All eleven implementation units in this PR are the existing src/*.cpp files with a leading #ifdef BOOST_PROGRAM_OPTIONS_MODULE_UNIT / module; ... #endif block, and CMake's scanner (/scanDependencies) handles them: they are recognized as implementation units and compiled against the interface BMI (validated in clean build trees; the module test links and passes). My reading is that the "module declarations must be first" rule is satisfied because only preprocessing directives precede them, and MSVC's scanner does evaluate the conditionals (the target's compile definitions are passed to the scan step). If you have a reference to compilers rejecting this, I'd like to test against it — that would rule out the no-new-files approach and force the separate .cc wrappers from the document.

Mixing #include <std_header> and import std;: on MSVC the order is what matters:

  • textual std includes first, then import std; later in the same global module fragment: works — MSVC reconciles the two;
  • import std; first, then textual std includes: fails with C2011/C2953 redefinitions (std::byte, std::_Add_reference, ...);
  • textual std in a module unit with no explicit import std;, where the std module arrives only transitively (via importing a primary interface that imported std): also fails with redefinitions — adding an explicit import std; after the textual includes in that unit fixes it;
  • macros: assert is not provided by import std;, of course; units calling assert() keep <cassert> textual (the document already covers this class of problem).

That is why every unit here places its standard-library guard after its textual dependency includes, and the one file whose std headers originally sit above the Boost includes issues its import std; in the closing block instead.

One more data point that may interest you: operators declared in GMF-preloaded dependency headers are not found by ADL in importing translation units on MSVC (C2679) — this interface re-exports boost::typeindex::operator==/!= explicitly, otherwise boost::any_cast comparisons instantiated in consumers (via variable_value::as<T>()) don't compile.

Feedback on modules.md for this library's situation (compiled library; Boost.Any/Function/SmartPtr/LexicalCast headers still textually included):

  • import boost.dep; can't apply yet, so every module unit preloads the dependency headers textually in its global module fragment (they stay attached to the global module, as in classic builds). The <boost/config/std/...> compatibility headers can't be used either (Adds support for C++20 modules config#535 unmerged), so the standard-library guards inline follow the PR#535 pattern, each with a TODO to swap to the real headers when they land;
  • wrapping each .cpp in a .cc file as the document suggests would duplicate the sources; making the existing .cpp double as implementation units behind #ifdef achieved the same without new files (see the scanner answer above);
  • CXX_MODULE_STD / import std also validated here: -DCMAKE_CXX_MODULE_STD=ON -DCMAKE_CXX_STANDARD=23 plus the CMake 4.3 experimental gate UUID builds and the consumer test passes in that mode too.

Happy to provide repro snippets for any of the MSVC behaviors if useful for the document.

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.

Provide C++20 module

3 participants