Repository navigation
Add optional C++20 module interface (import boost.program_options) - #161
soroshsabz wants to merge 17 commits into
Conversation
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.
|
@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.
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.
|
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
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? |
|
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 I've only had a quick glance, but some things to at least check:
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 Cheers, |
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 theboost::program_optionsnamespace and all existing code keep working unchanged.Design
src/cppm/boost/program_options.cppm— no per-name using-declarations, no new.cppfiles, no header modifications:<boost/program_options.hpp>in the module purview inside anexport { ... }block, exporting its declarations without touching any header.src/*.cppgains a leadingBOOST_PROGRAM_OPTIONS_MODULE_UNITconditional that, in the module build, makes the file a module implementation unit ofboost.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).src/*.cppwith 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::typeindexcomparison operators remain re-exported: the preloadedboost::any_castis instantiated in importing TUs and finds them by ADL only if exported (MSVC C2679 otherwise, verified three times).BOOST_PROGRAM_OPTIONS_VERSION) are not exported by C++20 modules; code that needs them keeps including the classic headers.Build system
BOOST_PROGRAM_OPTIONS_BUILD_MODULES=ONand produces an additionalBoost::program_options_modulestarget 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.b2does not support building C++20 module interfaces yet; this can be revisited once the build system grows module support.Tests
test/module_test.cppconsumes onlyimport 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 thebasic_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.mdgains a "C++20 module support" section, and a newdoc/modules.xmlpage documents usage, build requirements and limitations.