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.

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