From ad173b42e4626cf1291ce067ae2ed6881ce6b7c9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 4 Sep 2026 12:01:07 -0400 Subject: [PATCH 1/3] register_classes: record every ancestor, follow no alias, scan once Fixes from the review of ebe8cf1 (register classes by reflection). The records worked from a per-registration view of the program: a class was recorded with its nearest registered ancestors among the classes *this* scan found, so a derived class and an intermediate base registered by different registrations - a library and a plugin, each scanning its own namespace - lost the edge between them, and the intermediate's overriders were silently skipped for the derived class. The n x n matrix and the nearest-ancestor reduction behind this also exhausted the default -fconstexpr-ops-limit on a chain of sixty classes. Each class is now recorded with every registered class above it - the shape initialize already consumes, deriving the direct bases itself - and an ancestor the scan does not declare, in a namespace it does not enter or a specialization of a class template (which members_of never yields), gets a record of its own. The scan itself: - never goes through an alias. A namespace alias to an enclosing namespace was an infinite recursion, one into a namespace under boost defeated the exclusion, and an aliased namespace was scanned shallower than the same one reached directly. A class an alias named before the scan reached its declaring scope was never walked into, and its nested classes were lost, depending on scan order; - does not ask is_complete_type of an aliased specialization of a class template: the question instantiates it, and a `using Edge = std::pair` over a Node defined elsewhere made a C++17-valid translation unit fail to compile; - pushes the classes it declares without a search, and only those with bases. It was quadratic in the number of classes merely visible in the translation unit, and the default scan stopped compiling at about five hundred of them; - runs once per registration, not once per target registry; - treats repeated inheritance consistently: a class is registered under the bases it reaches unambiguously through public paths - Repeated : Left, Right, both deriving from Animal, is registered under both - where it was dropped or kept depending on the depth of the hierarchy, and the reference promised the former. A method's return type is no longer a root: std::ostream& armed the covariant-return check in initialize, which then aborted on an overrider returning std::ostringstream&. A covariant return type is registered like any other class, when it derives from a root or is listed, as in C++17. An incomplete listed class is a static_assert, not an exception thrown from constant evaluation that named no class. The explicit_class_registration policy is gone. All it did was make register_classes skip a registry, silently dropping the classes listed for it, and nothing in the tree exercised it: the error tests and snippets that carried it never call the macro, so they withhold their class under either standard as they did. Build: the CMake probe passed its -std flag through CMAKE_REQUIRED_FLAGS, which try_compile puts before the flag it derives from CMAKE_CXX_STANDARD (CMP0067), so any configure that set the standard failed with a false FATAL_ERROR. The reflection options are now directory-scoped, so the shared-library tests get them too, and those tests register by reflection like the rest of the suite, which puts the per-program inline registrar across module boundaries and under hidden visibility. BOOST_OPENMETHOD_EXPECT_REFLECTION makes a target that quietly falls back to C++17 fail to build. The CI reflection job builds test/dynamic_loading. Docs: the scan runs at the registrar's point of instantiation, the end of the translation unit on every current compiler, so "reflection sees only what precedes it" stated the opposite of what happens. The reason to put the macro last is the standard's: a selected class or method declared after the registrar makes the program ill-formed, no diagnostic required. The five-line @code block in the register_classes reference is restored. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JDLWpTRETW5kfEgK9b2fgF --- .github/workflows/ci.yml | 6 +- CMakeLists.txt | 56 +- ce/CMakeLists.txt | 1 - doc/modules/ROOT/examples/CMakeLists.txt | 2 - doc/modules/ROOT/pages/basics.adoc | 54 +- doc/modules/ROOT/pages/core_api.adoc | 4 +- .../ROOT/pages/registries_and_policies.adoc | 9 - doc/modules/ROOT/snippets/CMakeLists.txt | 1 - .../ROOT/snippets/errors_missing_base.cpp | 2 - .../snippets/errors_missing_class_call.cpp | 2 - .../snippets/errors_missing_class_method.cpp | 2 - .../errors_missing_class_overrider.cpp | 2 - .../ROOT/snippets/explicit_registration.hpp | 33 -- include/boost/openmethod/core.hpp | 341 ++++++------ .../boost/openmethod/detail/reflection.hpp | 407 ++++++++++----- include/boost/openmethod/macros.hpp | 9 +- include/boost/openmethod/preamble.hpp | 45 +- test/CMakeLists.txt | 3 - ...mpile_fail_reflection_incomplete_class.cpp | 32 ++ test/dynamic_loading/main.cpp | 7 +- test/dynamic_loading/method.cpp | 7 +- test/dynamic_loading/overrider.cpp | 7 +- test/dynamic_loading/registry.cpp | 7 +- .../custom_registry/lib.cpp | 7 +- .../custom_registry/lib2.cpp | 7 +- .../custom_registry/main.cpp | 7 +- .../default_registry/lib.cpp | 7 +- .../default_registry/main.cpp | 7 +- test/test_capture_errors.hpp | 14 - test/test_checked_registry.hpp | 15 - ..._class_registration_missing_base_class.cpp | 4 - ...s_registration_unknown_class_overrider.cpp | 4 - test/test_classes.hpp | 15 +- test/test_custom_rtti_deferred.cpp | 2 +- test/test_custom_rtti_simple.cpp | 2 +- test/test_custom_rtti_simple_projection.cpp | 2 +- test/test_custom_rtti_virtual_base.cpp | 2 +- test/test_dispatch_across_namespaces.cpp | 2 +- test/test_dispatch_boost_any.cpp | 2 +- test/test_dispatch_comma_in_return_type.cpp | 2 +- test/test_dispatch_intrusive_ptr.cpp | 2 +- test/test_dispatch_lvalue_refs.cpp | 2 +- test/test_dispatch_multi.cpp | 2 +- test/test_dispatch_next_fn.cpp | 2 +- test/test_dispatch_pointer.cpp | 2 +- test/test_dispatch_rvalue_refs.cpp | 2 +- test/test_dispatch_shared_ptr_by_ref.cpp | 2 +- test/test_dispatch_shared_ptr_by_value.cpp | 2 +- test/test_dispatch_std_any.cpp | 2 +- test/test_dispatch_unique_ptr.cpp | 2 +- test/test_n2216_covariant_return_type.cpp | 2 +- test/test_n2216_pick_any_ambiguous.cpp | 2 +- test/test_namespaces.cpp | 2 +- test/test_pointer_to_method.cpp | 2 +- test/test_reflection.cpp | 493 ++++++++++++++++-- test/test_rolex.cpp | 2 +- test/test_runtime_errors_bad_call.cpp | 2 +- .../test_runtime_errors_bad_call_type_ids.cpp | 2 +- ...ime_errors_bad_call_type_ids_smart_ptr.cpp | 2 +- ...est_runtime_errors_duplicate_overrider.cpp | 2 +- ...untime_errors_initialize_unknown_class.cpp | 4 - test/test_runtime_errors_throw_error.cpp | 2 +- ...test_smart_virtual_ptr_value_semantics.cpp | 2 +- test/test_static_rtti.cpp | 2 +- test/test_virtual_any_boost.cpp | 2 +- test/test_virtual_any_std.cpp | 2 +- test/test_virtual_ptr_by_ref.cpp | 2 +- test/test_virtual_ptr_non_polymorphic.cpp | 2 +- test/test_virtual_ptr_shared_by_const_ref.cpp | 2 +- test/test_virtual_ptr_shared_by_value.cpp | 2 +- test/test_virtual_ptr_unique.cpp | 2 +- 71 files changed, 1135 insertions(+), 560 deletions(-) delete mode 100644 doc/modules/ROOT/snippets/explicit_registration.hpp create mode 100644 test/compile_fail_reflection_incomplete_class.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df61ab65..e4dec4c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,12 +84,16 @@ jobs: # The suite is the reflection test: BOOST_OPENMETHOD_TEST_CLASSES expands # to nothing here, so every class has to be found by the scan that - # BOOST_OPENMETHOD_REGISTER_CLASSES starts. + # BOOST_OPENMETHOD_REGISTER_CLASSES starts. BUILD_SHARED_LIBS brings in + # test/dynamic_loading, where the registrar that scan instantiates - one + # inline variable per class, for the whole program - crosses module + # boundaries. - name: Build and test run: | cmake -S . -B ../build -G Ninja \ -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_COMPILER=g++-16 \ + -DBUILD_SHARED_LIBS=ON \ -DBOOST_OPENMETHOD_ENABLE_REFLECTION=ON \ -DBOOST_OPENMETHOD_BUILD_TESTS=ON \ -DBOOST_OPENMETHOD_WARNINGS_AS_ERRORS=ON \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 47ea21b1..d9d28038 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,16 +65,31 @@ option( OFF) # C++26 reflection (P2996). The library detects it on its own, from -# __cpp_impl_reflection; this only arranges for the tests to be built in a mode -# where the compiler provides it, which needs both C++26 and, on GCC, an opt-in -# flag. It is applied per target rather than through CMAKE_CXX_FLAGS, because -# CMake probes the compiler before CMAKE_CXX_STANDARD takes effect and GCC -# rejects -freflection under any other standard. +# __cpp_impl_reflection; this only arranges for the tests and examples to be +# built in a mode where the compiler provides it, which needs both C++26 and, on +# GCC, an opt-in flag. The options are added to this directory - every test, +# example and snippet, the shared-library ones in the subdirectories included - +# rather than to CMAKE_CXX_FLAGS, because CMake probes the compiler before +# CMAKE_CXX_STANDARD takes effect and GCC rejects -freflection under any other +# standard. set(BOOST_OPENMETHOD_REFLECTION_OPTIONS "") if (BOOST_OPENMETHOD_ENABLE_REFLECTION) include(CheckCXXSourceCompiles) + # The probe passes its own -std flag, and try_compile appends the one it + # derives from CMAKE_CXX_STANDARD *after* CMAKE_REQUIRED_FLAGS (policy + # CMP0067), where it would win: a configure that sets the standard - a + # preset, a toolchain file, a consuming project - would fail both candidates + # and stop at the FATAL_ERROR below. Take the variables out of the probe's + # way, and put them back after. Set to nothing rather than unset: a normal + # variable hides a cache one - a -D on the command line - where unset would + # expose it, and try_compile leaves an empty standard alone. + set(BOOST_OPENMETHOD_SAVED_CXX_STANDARD "${CMAKE_CXX_STANDARD}") + set(BOOST_OPENMETHOD_SAVED_CXX_EXTENSIONS "${CMAKE_CXX_EXTENSIONS}") + set(CMAKE_CXX_STANDARD "") + set(CMAKE_CXX_EXTENSIONS "") + set(BOOST_OPENMETHOD_REFLECTION_TEST_SOURCE [[ #include struct Base {}; @@ -107,6 +122,16 @@ if (BOOST_OPENMETHOD_ENABLE_REFLECTION) unset(CMAKE_REQUIRED_FLAGS) unset(CMAKE_REQUIRED_QUIET) + # An empty one is unset, not set to nothing, which a target would take for + # an invalid standard. + foreach(var CMAKE_CXX_STANDARD CMAKE_CXX_EXTENSIONS) + if ("${BOOST_OPENMETHOD_SAVED_${var}}" STREQUAL "") + unset(${var}) + else() + set(${var} "${BOOST_OPENMETHOD_SAVED_${var}}") + endif() + endforeach() + if (NOT BOOST_OPENMETHOD_HAS_REFLECTION) message( FATAL_ERROR @@ -118,22 +143,17 @@ if (BOOST_OPENMETHOD_ENABLE_REFLECTION) STATUS "Boost.OpenMethod: C++26 reflection enabled" " [${BOOST_OPENMETHOD_REFLECTION_OPTIONS}]") -endif() - -# Build `target` with C++26 reflection, if BOOST_OPENMETHOD_ENABLE_REFLECTION is -# ON. Does nothing otherwise, so callers need no condition of their own. -function(boost_openmethod_enable_reflection target) - if (NOT BOOST_OPENMETHOD_ENABLE_REFLECTION) - return() - endif() # The standard flag is passed here rather than through CXX_STANDARD: CMake # learned the value 26 only in 3.30, and this project supports older ones. - # target_compile_options come after the flag CMake derives from the - # library's cxx_std_17 requirement, and the last -std wins. - target_compile_options( - ${target} PRIVATE ${BOOST_OPENMETHOD_REFLECTION_OPTIONS}) -endfunction() + # Directory compile options come after the flag CMake derives from the + # library's cxx_std_17 requirement, and the last -std wins. The definition + # makes a target that does not end up with reflection fail to build, instead + # of quietly falling back to explicit class registration: see + # test/test_classes.hpp. + add_compile_options(${BOOST_OPENMETHOD_REFLECTION_OPTIONS}) + add_compile_definitions(BOOST_OPENMETHOD_EXPECT_REFLECTION) +endif() if (BOOST_OPENMETHOD_BUILD_EXAMPLES AND NOT BOOST_OPENMETHOD_BUILD_TESTS) message( diff --git a/ce/CMakeLists.txt b/ce/CMakeLists.txt index 039b772c..cb7b62e1 100644 --- a/ce/CMakeLists.txt +++ b/ce/CMakeLists.txt @@ -18,7 +18,6 @@ foreach (cpp ${cpp_files}) get_filename_component(stem ${cpp} NAME_WE) set(test_target "boost_openmethod-ce-${stem}") add_executable(${test_target} ${cpp}) - boost_openmethod_enable_reflection(${test_target}) target_link_libraries(${test_target} PRIVATE Boost::openmethod) add_test(NAME ${test_target} COMMAND ${test_target}) add_dependencies(tests ${test_target}) diff --git a/doc/modules/ROOT/examples/CMakeLists.txt b/doc/modules/ROOT/examples/CMakeLists.txt index 2b0555f2..f922ab9e 100644 --- a/doc/modules/ROOT/examples/CMakeLists.txt +++ b/doc/modules/ROOT/examples/CMakeLists.txt @@ -21,7 +21,6 @@ foreach (cpp ${cpp_files}) get_filename_component(stem ${cpp} NAME_WE) set(test_target "boost_openmethod-${stem}") add_executable(${test_target} ${cpp}) - boost_openmethod_enable_reflection(${test_target}) target_link_libraries(${test_target} PRIVATE Boost::openmethod Boost::unit_test_framework) add_test(NAME ${test_target} COMMAND ${test_target}) add_dependencies(tests ${test_target}) @@ -44,7 +43,6 @@ function(boost_openmethod_add_step_by_step dir) file(GLOB cpp_files "${subdir}/*.cpp") set(target "boost_openmethod-${dir}_${subex}") add_executable(${target} ${cpp_files}) - boost_openmethod_enable_reflection(${target}) target_link_libraries(${target} PRIVATE Boost::openmethod) set(output_dir openmethod/${dir}/${subex}) set_target_properties(${target} PROPERTIES diff --git a/doc/modules/ROOT/pages/basics.adoc b/doc/modules/ROOT/pages/basics.adoc index 2453cc95..2d949a48 100644 --- a/doc/modules/ROOT/pages/basics.adoc +++ b/doc/modules/ROOT/pages/basics.adoc @@ -175,19 +175,33 @@ rooted in some general-purpose base contributes only the part of itself that takes part in dispatch. As soon as another method does dispatch on that base, it is registered, and the inheritance edges through it with it. +The namespaces decide where the *derived* classes are looked for, not which +bases are recorded. A class between a registered class and a root is registered +too, wherever it is declared - in a namespace the scan does not enter, or as a +specialization of a class template, which the scan cannot find on its own - so +that the inheritance lattice is complete. And each class is recorded with every +registered class above it, so that a registration stands on its own, whatever +another one - in another translation unit, over other namespaces - registers +alongside it. + A scan does not descend into the `std` and `boost` namespaces, so scanning the global namespace costs little more than a narrower one would - a method cannot dispatch on a class the program never heard of anyway. The exclusion applies to recursion only: a namespace *listed* explicitly is always scanned, which is how a class in `std` or `boost` gets registered. -Reflection sees only what precedes it, so the macro must come *after* the -declarations it is meant to find. Putting it at the bottom of the file is the -simplest way to be sure. +The scan runs when the registrar the macro declares is instantiated, which +current compilers do at the end of the translation unit: it sees the whole +file, wherever the macro sits. The standard promises less. A class or a method +the scan would select, declared after the macro in the same translation unit, +makes the program ill-formed, no diagnostic required. So put the macro *after* +the declarations it is meant to find: at the bottom of the file. Virtual and multiple inheritance are supported. Unlike `BOOST_OPENMETHOD_CLASSES`, which rejects it, repeated inheritance is not an -error here: an ambiguous base cannot take part in dispatch, so it is left out. +error here: a class is recorded under the bases it reaches unambiguously +through public inheritance, and a base it inherits more than once - which no +reference to it can be converted to - is left out of its list. Without reflection - in C++17, or in C++26 without the compiler flag that enables it - the macro expands to nothing. A file that also calls @@ -199,26 +213,22 @@ A method is found through any declaration that names its `method` type: the alias `BOOST_OPENMETHOD` declares alongside the method, a `using` declaration of your own, or any of the method's registrar objects. None of those depends on the method having an overrider, so a method declared with `BOOST_OPENMETHOD` is -always found. - -Three situations remain outside the scan's reach, and need a -`BOOST_OPENMETHOD_CLASSES` of their own: - -* a class in a namespace the macro does not scan; +always found. A method's return type is not a root: a covariant return type is +registered like any other class, when it derives from a root, or when it is +listed. + +Four situations remain outside the scan's reach, and need to be registered by +other means - listed in the macro, or given a `BOOST_OPENMETHOD_CLASSES` of +their own: + +* a class in a namespace the macro does not scan, unless it sits between a + class the scan finds and a root; +* a specialization of a class template that no method dispatches on, and that + no class the scan finds derives from - one used only as an overrider + parameter, say. Specializations are not members of a namespace, so the scan + does not see them; list it: `BOOST_OPENMETHOD_REGISTER_CLASSES({^^Pet})`; * a core API method whose `method<...>` type is spelled out in full at every use, with no `using` declaration of its own and no overrider - nothing names it; * a program that declares no method at all, and uses `virtual_ptr` on its own - there is no virtual parameter for the scan to start from. - -### Turning It Off - -Adding the `policies::explicit_class_registration` policy to a registry stops -the library from registering anything on its own, in C++26 as in C++17: - -[source,c++] ----- -struct my_registry - : boost::openmethod::default_registry::with< - boost::openmethod::policies::explicit_class_registration> {}; ----- diff --git a/doc/modules/ROOT/pages/core_api.adoc b/doc/modules/ROOT/pages/core_api.adoc index 396a620f..a79afc25 100644 --- a/doc/modules/ROOT/pages/core_api.adoc +++ b/doc/modules/ROOT/pages/core_api.adoc @@ -90,8 +90,8 @@ BOOST_OPENMETHOD_REGISTER(register_classes<>); A method declared the way `postfix` is above - an alias for a `method` specialization - is found directly, and so is any of its `override` registrars. -Reflection sees only what precedes it, so this must come after the declarations -it is meant to find. See +The registration must come after the declarations it is meant to find, at the +bottom of the file. See xref:ROOT:basics.adoc#registering_classes_by_reflection[Registering Classes by Reflection]. diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc index 7f9085b8..db0f9ed0 100644 --- a/doc/modules/ROOT/pages/registries_and_policies.adoc +++ b/doc/modules/ROOT/pages/registries_and_policies.adoc @@ -92,15 +92,6 @@ is defined, `default_registry` also contains the `runtime_checks` policy. This enables extra validations during method dispatch, which can detect missing class registrations that could not be caught by `initialize`. -The `explicit_class_registration` policy does the opposite of adding a -behaviour: it stops the library from registering classes by reflection, so a -registry that contains it knows only the classes named in -xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES] or -cpp:use_classes[]. It has no effect if the compiler does not support C++26 -reflection. See -xref:ROOT:basics.adoc#registering_classes_by_reflection[Registering Classes by -Reflection]. - The library provides another predefined registry: cpp:indirect_registry[]. It is useful when shared libraries are dynamically loaded at runtime, and add methods and overriders across program and shared library boundaries. See the section diff --git a/doc/modules/ROOT/snippets/CMakeLists.txt b/doc/modules/ROOT/snippets/CMakeLists.txt index de47749b..1b7b8c3e 100644 --- a/doc/modules/ROOT/snippets/CMakeLists.txt +++ b/doc/modules/ROOT/snippets/CMakeLists.txt @@ -23,7 +23,6 @@ foreach (cpp ${cpp_files}) get_filename_component(stem ${cpp} NAME_WE) set(test_target "boost_openmethod-snippet_${stem}") add_executable(${test_target} ${cpp}) - boost_openmethod_enable_reflection(${test_target}) target_link_libraries(${test_target} PRIVATE Boost::openmethod Boost::unit_test_framework) add_test(NAME ${test_target} COMMAND ${test_target}) add_dependencies(tests ${test_target}) diff --git a/doc/modules/ROOT/snippets/errors_missing_base.cpp b/doc/modules/ROOT/snippets/errors_missing_base.cpp index 20288f4f..fc557b5e 100644 --- a/doc/modules/ROOT/snippets/errors_missing_base.cpp +++ b/doc/modules/ROOT/snippets/errors_missing_base.cpp @@ -3,8 +3,6 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) -#include "explicit_registration.hpp" - #include #include diff --git a/doc/modules/ROOT/snippets/errors_missing_class_call.cpp b/doc/modules/ROOT/snippets/errors_missing_class_call.cpp index 9b5d42f3..5f6a2bc1 100644 --- a/doc/modules/ROOT/snippets/errors_missing_class_call.cpp +++ b/doc/modules/ROOT/snippets/errors_missing_class_call.cpp @@ -7,8 +7,6 @@ // which `default_registry` carries only when this symbol is defined. #define BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS -#include "explicit_registration.hpp" - #include #include diff --git a/doc/modules/ROOT/snippets/errors_missing_class_method.cpp b/doc/modules/ROOT/snippets/errors_missing_class_method.cpp index e47d28ba..4c4095c5 100644 --- a/doc/modules/ROOT/snippets/errors_missing_class_method.cpp +++ b/doc/modules/ROOT/snippets/errors_missing_class_method.cpp @@ -3,8 +3,6 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) -#include "explicit_registration.hpp" - #include #include diff --git a/doc/modules/ROOT/snippets/errors_missing_class_overrider.cpp b/doc/modules/ROOT/snippets/errors_missing_class_overrider.cpp index 65652dfa..31ad6bbb 100644 --- a/doc/modules/ROOT/snippets/errors_missing_class_overrider.cpp +++ b/doc/modules/ROOT/snippets/errors_missing_class_overrider.cpp @@ -3,8 +3,6 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) -#include "explicit_registration.hpp" - #include #include diff --git a/doc/modules/ROOT/snippets/explicit_registration.hpp b/doc/modules/ROOT/snippets/explicit_registration.hpp deleted file mode 100644 index b2066bb3..00000000 --- a/doc/modules/ROOT/snippets/explicit_registration.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2017-2026 Jean-Louis Leroy -// Distributed under the Boost Software License, Version 1.0. -// See accompanying file LICENSE_1_0.txt -// or copy at http://www.boost.org/LICENSE_1_0.txt) - -// Makes the default registry require explicit class registration, so that the -// error snippets keep reporting the error they illustrate when the compiler -// supports C++26 reflection. Include *before* . Like -// error_harness.hpp, never part of a tagged region: the pages show the mistake -// and the operation that reports it, and nothing else. -// -// The errors themselves do not go away in C++26 -- a class the library cannot -// reach from a method signature, an overrider, or a virtual_ptr still has to be -// registered by hand -- but these particular examples are all within its reach. - -#ifndef BOOST_OPENMETHOD_SNIPPETS_EXPLICIT_REGISTRATION_HPP -#define BOOST_OPENMETHOD_SNIPPETS_EXPLICIT_REGISTRATION_HPP - -// Like test_capture_errors.hpp, this header owns the whole recipe: the -// declaration, the BOOST_OPENMETHOD_DEFAULT_REGISTRY definition, the library -// include, and the registry itself. A pre-core header of the library - -// preamble.hpp, default_registry.hpp - belongs nowhere outside include/. - -struct snippet_registry; -#define BOOST_OPENMETHOD_DEFAULT_REGISTRY snippet_registry - -#include - -struct snippet_registry : - boost::openmethod::default_registry::with< - boost::openmethod::policies::explicit_class_registration> {}; - -#endif diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index cad8a165..a826911a 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -516,11 +516,11 @@ namespace detail { template inline use_class_aux reflected_class_registrar; -// Register every class the scan selected, each with its direct bases, as -// `reflected_registered_classes` computed them. +// Register every class the scan selected for `Registry`, each with its +// registered ancestors, as `reflected_registered_classes` computed them. template -BOOST_FORCEINLINE auto use_reflected_classes(mp11::mp_list*) - -> void { +BOOST_FORCEINLINE auto use_reflected_classes( + mp11::mp_list*) -> void { (..., (void)&reflected_class_registrar); } @@ -2797,14 +2797,14 @@ struct method_traits_aux; template< typename Id, typename ReturnType, typename... Parameters, class Registry> struct method_traits_aux> { - // The classes the method dispatches on, plus its return type, which is - // registered too when it is covariant. Same expression as - // `method::resolve_type_ids`. - using type = mp11::mp_push_back< - mp11::mp_transform_q< - mp11::mp_bind_back, - virtual_types>>, - virtual_type>; + // The classes the method dispatches on. Not its return type: a covariant + // return type is registered when the scan finds it deriving from one of + // these, or when it is listed - as in C++17 - not as a root of its own. + // A root return type would make `initialize` demand a registration for + // the return type of every overrider, `std::ostringstream` included. + using type = mp11::mp_transform_q< + mp11::mp_bind_back, + virtual_types>>; }; // Read from reflection, by `substitute`-ing a method into it and taking the @@ -2977,172 +2977,180 @@ using register_classes_registries = typename [: register_classes_registries_info() :]; // clang-format on -// The classes to register for the argument groups `Groups`, each with its -// direct bases: the classes the methods of `Registry` dispatch on, the classes -// the groups list, and the ones a scan of the listed namespaces found that -// derive from them. Returns `mp_list, ...>` - the -// shape `use_class_aux` expects, with the class repeated as its own improper -// base, as `inheritance_map` produces. -// -// The work is done here, in reflection, and not with `mp11` over the lists the -// scan produces. A scan of the global namespace reaches every class in the -// program that is not in `std` or `boost`, and instantiating a trait once per -// pair of them costs far more than walking their base classes does. -template -consteval auto reflected_registered_classes_info() -> std::meta::info { - // Partition the groups. Registries take no part here: the caller calls - // this function once per registry. - auto namespaces = - register_classes_items(register_classes_kind::namespaces); - auto virtual_classes = +// True if every listed class is complete. An incomplete one - a forward +// declaration left in a group by mistake - would otherwise surface as an +// exception thrown from inside constant evaluation, which names no class. +template +consteval auto register_classes_classes_are_complete() -> bool { + auto types = register_classes_items(register_classes_kind::classes); - for (auto& type : virtual_classes) { - type = std::meta::remove_cv(type); + for (auto type : types) { + if (!std::meta::is_complete_type(type)) { + return false; + } } - // With no namespace to start from, scan the global namespace. Listing - // classes does not change that: they are extra roots for the scan, not a - // way to turn it off. - if (namespaces.empty()) { - namespaces.push_back(^^::); + return true; +} + +// The scan for the argument groups `Groups`: the listed namespaces - or the +// global one - for the `method` specializations they name and the classes +// they declare. +template +consteval auto reflected_scan_of() -> reflected_scan { + auto listed = + register_classes_items(register_classes_kind::classes); + + for (auto& type : listed) { + type = std::meta::remove_cv(type); } - std::vector methods, classes; + return scan_namespaces( + register_classes_items(register_classes_kind::namespaces), + listed, ^^method); +} - for (auto ns : namespaces) { - scan_scope(ns, ^^method, methods, classes); +// Append to `entries` the record `use_class_aux` expects for `type` - the +// class, itself again as its own improper base, and every registered class +// above it - if `type` is registered at all: if it is one of `roots`, or +// reaches one through public inheritance. The classes above it that are +// registered are the ones that reach a root themselves, less the ones that +// repeated inheritance makes ambiguous, which `is_dispatchable_base` tells +// apart. +// +// Every registered ancestor is recorded, not only the direct bases. +// `initialize` derives the direct bases from the closure of what the records +// say, so a record that carries the whole ancestry stands on its own, whatever +// another registration - in another translation unit, over other namespaces - +// records alongside it. Keeping only the nearest ancestors would need the +// registered set to be the same everywhere, and it is not. +// +// An ancestor the scan does not declare - in a namespace it does not enter, a +// specialization of a class template, a local class - is given a record of its +// own here, recursively, as nothing else would. A class the scan declares, or +// finds through an alias, gets its record when its own turn comes. `extra` +// holds the ones done here, so that each is done once. +consteval void reflected_class_entries( + std::meta::info type, const std::vector& roots, + const reflected_scan& scan, std::vector& extra, + std::vector& entries) { + std::vector bases; + + if (!walk_bases(type, roots, bases)) { + return; } - // Add the classes the methods dispatch on. + std::vector entry; + entry.push_back(type); + // The class as its own improper base, as `inheritance_map` does. + // `initialize` discards it, and `use_class_aux` cannot hold an empty base + // array. + entry.push_back(type); + + for (std::size_t index = 1; index != bases.size(); ++index) { + const auto& base = bases[index]; - for (auto found : methods) { - // A method's third template argument is its registry. - if (std::meta::template_arguments_of(found)[2] != ^^Registry) { + if (!base.reaches_root) { continue; } - auto list = std::meta::dealias(std::meta::substitute( - ^^method_classes, - { - found})); - - for (auto type : std::meta::template_arguments_of(list)) { - // A method's return type is `void` unless it is covariant, and a - // virtual parameter may be a smart pointer rather than a class. - if (std::meta::is_class_type(type)) { - push_unique(virtual_classes, std::meta::remove_cv(type)); - } + if (base.repeated && !is_dispatchable_base(type, base.type)) { + continue; } - } - // Those, plus every class the scan found that derives from one of them. A - // base class no method dispatches on is left out: no overrider could ever - // be selected on it, and it would cost a lattice node, a hash slot and - // dispatch table space. - auto registered = virtual_classes; + entry.push_back(base.type); - for (auto found : classes) { - std::vector bases; - collect_dispatchable_bases(found, bases); - - for (auto base : bases) { - if (contains(virtual_classes, base)) { - push_unique(registered, found); - break; - } + if (!contains(roots, base.type) && !contains(scan.aliased, base.type) && + !scan_declares(base.type, scan.namespaces) && + !contains(extra, base.type)) { + extra.push_back(base.type); + reflected_class_entries(base.type, roots, scan, extra, entries); } } - // Which registered class inherits from which, as a square matrix indexed - // by position in `registered`. Walking the base classes once per class and - // answering from the matrix afterwards keeps this within the compiler's - // budget for constant evaluation: the alternative, re-searching a class' - // bases for every pair, is cubic in the number of classes times the depth - // of the hierarchy, and exceeds GCC's default -fconstexpr-ops-limit on a - // chain of a few dozen. - auto count = registered.size(); - std::vector inherits(count * count, char(0)); + entries.push_back(std::meta::substitute(^^mp11::mp_list, entry)); +} + +// The classes to register for the argument groups `Groups`, in each of +// `Registries`, an `mp_list` of registries. Returns +// `mp_list...>...>`: one list +// per registry, headed by the registry, holding the records `use_class_aux` +// expects. The scan runs once, for all the registries: only the roots depend on +// the registry - the classes the groups list, and the classes the registry's +// methods dispatch on. Registered are the roots, and every class the scan +// found, or was led to, that derives from one. +// +// The work is done here, in reflection, and not with `mp11` over the lists the +// scan produces. A scan of the global namespace reaches every class in the +// program that is not in `std` or `boost`, and instantiating a trait once per +// pair of them costs far more than walking their base classes does. +template +consteval auto reflected_registered_classes_info() -> std::meta::info { + auto scan = reflected_scan_of(); + auto registries = std::meta::template_arguments_of(^^Registries); + std::vector lists; - for (auto index = 0u; index != count; ++index) { - std::vector bases; - collect_dispatchable_bases(registered[index], bases); + for (auto registry : registries) { + auto roots = scan.listed; - for (auto base : bases) { - if (base == registered[index]) { + for (auto found : scan.methods) { + // A method's third template argument is its registry. + if (std::meta::template_arguments_of(found)[2] != registry) { continue; } - for (auto other = 0u; other != count; ++other) { - if (registered[other] == base) { - inherits[index * count + other] = char(1); - break; - } + auto list = std::meta::dealias( + std::meta::substitute( + ^^method_classes, + { + found})); + auto types = std::meta::template_arguments_of(list); + + for (auto type : types) { + push_unique(roots, std::meta::remove_cv(type)); } } - } - std::vector entries; + std::vector entries, extra; + entries.push_back(registry); - for (auto index = 0u; index != count; ++index) { - std::vector entry; - entry.push_back(registered[index]); - // The class as its own improper base, as `inheritance_map` does. - // `initialize` discards it, and `use_class_aux` cannot hold an empty - // base array. - entry.push_back(registered[index]); - - for (auto base = 0u; base != count; ++base) { - if (!inherits[index * count + base]) { - continue; + for (auto root : roots) { + // A method written by hand may be declared on a class that this + // translation unit never completes. It is registered where it is. + if (std::meta::is_complete_type(root)) { + reflected_class_entries(root, roots, scan, extra, entries); } + } - // Keep only the nearest ancestors - the direct bases of this class - // in the lattice the registry will hold. One that another ancestor - // also inherits from is reached through that one, and recording it - // as well would make `initialize` see an edge that is not there. - // Unregistered classes in between are skipped over, which is what - // flattens the lattice down to the classes that dispatch. - bool hidden = false; - - for (auto between = 0u; between != count; ++between) { - if (between != base && inherits[index * count + between] && - inherits[between * count + base]) { - hidden = true; - break; - } + for (auto type : scan.declared) { + if (!contains(roots, type)) { + reflected_class_entries(type, roots, scan, extra, entries); } + } - if (!hidden) { - entry.push_back(registered[base]); + for (auto type : scan.aliased) { + if (!contains(roots, type)) { + reflected_class_entries(type, roots, scan, extra, entries); } } - entries.push_back(std::meta::substitute(^^mp11::mp_list, entry)); + lists.push_back(std::meta::substitute(^^mp11::mp_list, entries)); } - return std::meta::substitute(^^mp11::mp_list, entries); + return std::meta::substitute(^^mp11::mp_list, lists); } -// `mp_list, ...>`, ready for `use_class_aux`. +// `mp_list...>...>`, ready +// for `use_reflected_classes`. // // clang-format off: the formatter predates P2996 and eats the spaces around the // splice, leaving `typename[:...:]`. -template +template using reflected_registered_classes = - typename [: reflected_registered_classes_info() :]; + typename [: reflected_registered_classes_info() :]; // clang-format on -// Register the classes the groups select in one registry - unless it opted out -// of reflection-based registration, in which case the scan does not even run. -template -BOOST_FORCEINLINE auto use_reflected_classes_in() -> void { - if constexpr (Registry::has_reflected_class_registration) { - using registered = reflected_registered_classes; - use_reflected_classes(static_cast(nullptr)); - } -} - #endif } // namespace detail @@ -3166,20 +3174,25 @@ class register_classes { static_assert( detail::register_classes_groups_are_ordered(), "order the groups as namespaces, classes, registries"); + static_assert( + detail::register_classes_classes_are_complete(), + "a listed class must be complete"); using found_registries = detail::register_classes_registries; using registries = mp11::mp_if< mp11::mp_empty, mp11::mp_list, found_registries>; - template - static auto use(mp11::mp_list*) -> void { - (..., detail::use_reflected_classes_in()); + template + static auto use(mp11::mp_list*) -> void { + (..., detail::use_reflected_classes(static_cast(nullptr))); } public: register_classes() { - use(static_cast(nullptr)); + use(static_cast< + detail::reflected_registered_classes*>( + nullptr)); } }; @@ -3204,15 +3217,18 @@ class register_classes { //! @li **Namespaces** to scan: `{^^app, ^^zoo}`, `{\^^::}`. //! @li **Classes** to register: `{^^Animal}`. They are registered whether a //! method dispatches on them or not, and they are extra roots for the scan, -//! which registers the classes it finds deriving from them. +//! which registers the classes it finds deriving from them. A listed class +//! must be complete. //! @li **Registries** to register the classes in: `{^^my_registry}`. Each one //! receives the registration. The default is //! `BOOST_OPENMETHOD_DEFAULT_REGISTRY`. //! //! @code -//! register_classes<^^app, ^^my_registry> register_classes<{^^app, ^^zoo}, -//! {^^r1, ^^r2}> register_classes<{^^Animal, ^^Dog}> -//! register_classes<^^my_registry> register_classes<> +//! register_classes<^^app, ^^my_registry> +//! register_classes<{^^app, ^^zoo}, {^^r1, ^^r2}> +//! register_classes<{^^Animal, ^^Dog}> +//! register_classes<^^my_registry> +//! register_classes<> //! @endcode //! //! A group holding two kinds of reflection is an error, and so is a group out @@ -3221,24 +3237,36 @@ class register_classes { //! registration. //! //! The scan covers the listed namespaces, the namespaces nested in them, and -//! the classes nested in the classes it finds, except `std` and `boost`: +//! the classes nested in the classes it declares, except `std` and `boost`: //! walking those would cost a great deal and find nothing, as a method cannot //! be declared on a class the program has never heard of. The exclusion applies //! to recursion only, so a class in `std` or `boost` is registered by *listing* -//! the namespace it is in. A class is also found through an alias that names -//! it, but the scan does not walk *into* an alias: only a class the scanned -//! scope declares is descended into. The scan finds the methods of each target -//! registry, collects the classes they dispatch on, and registers those, the -//! listed classes, and every class in the scanned namespaces that derives from -//! one of them. A base class that no method dispatches on, and that is not +//! the namespace it is in. The scan never goes through an alias: a namespace +//! alias is not entered, and a type alias registers the class it names but is +//! not walked into. The scan finds the methods of each target registry and +//! collects the classes they dispatch on; those, and the listed classes, are +//! the *roots*. It registers the roots, and every class it finds that derives +//! from one of them. A base class that no method dispatches on, and that is not //! listed, is not registered: it could never be selected on. //! +//! A class between a registered class and a root is registered too, wherever +//! it is declared: in a namespace the scan does not enter, or as a +//! specialization of a class template, which the scan cannot find on its own. +//! The namespaces decide where the *derived* classes are looked for, not which +//! bases are recorded; and each class is recorded with every registered class +//! above it, so that a registration stands on its own, whatever another one, +//! in another translation unit, registers alongside it. +//! //! If no namespace is given, the global namespace is scanned - whatever the //! other groups hold, and for @ref BOOST_OPENMETHOD_REGISTER_CLASSES too. Name //! a namespace to scan only that one. //! -//! Reflection sees only what precedes it, so `register_classes` must come -//! **after** the declarations it is meant to find - at the bottom of the file. +//! The scan runs when the registrar is instantiated, which current compilers +//! do at the end of the translation unit: it sees the whole file, wherever the +//! registrar sits. The standard promises less. A class or a method the scan +//! would select, declared after the registrar in the same translation unit, +//! makes the program ill-formed, no diagnostic required. Put the registrar +//! **after** the declarations it is meant to find, at the bottom of the file. //! //! A method is found through any namespace member that names its `method` //! specialization: the alias @ref BOOST_OPENMETHOD declares for it, a `using` @@ -3247,13 +3275,16 @@ class register_classes { //! those requires the method to have an overrider. A core interface method //! whose `method<...>` type is spelled out in full at every use, with neither a //! `using` declaration nor an overrider, is named by nothing and is not found; -//! its classes must be registered with @ref use_classes. +//! its classes must be registered with @ref use_classes. A method's return +//! type is not a root: a covariant return type is registered like any other +//! class, when it derives from a root, or when it is listed. //! //! Virtual and multiple inheritance are supported. Unlike @ref use_classes, -//! which rejects it, repeated inheritance is not an error here: a base a class -//! inherits more than once cannot be converted to, so it cannot take part in -//! that class' dispatch, and it is left out of its bases. A class left with no -//! registered base is not registered at all. +//! which rejects it, repeated inheritance is not an error here: a class is +//! recorded under the bases it reaches unambiguously through public +//! inheritance, and a base it inherits more than once, which no reference to +//! it can be converted to, is left out of its list. A class with no registered +//! class above it is not registered at all. //! //! This class template is available only if the compiler supports C++26 //! reflection, i.e. if `BOOST_OPENMETHOD_HAS_REFLECTION` is 1. diff --git a/include/boost/openmethod/detail/reflection.hpp b/include/boost/openmethod/detail/reflection.hpp index a4eec0d6..0a74fe92 100644 --- a/include/boost/openmethod/detail/reflection.hpp +++ b/include/boost/openmethod/detail/reflection.hpp @@ -77,25 +77,113 @@ consteval void push_unique( } } +// ============================================================================= +// namespaces + +// True if `ns` is a namespace that a recursive scan does not enter: `std` and +// `boost`. Walking them would cost a great deal and find nothing: a method +// cannot be declared on a class the program has never heard of. The nested +// namespaces - `std::chrono`, `boost::mp11`, the inline versioning ones - are +// reached only through their parent, so they are left out with it. The +// exclusion applies only to recursion: a namespace listed explicitly is always +// scanned, which is how a class in `std` or `boost` is registered. +consteval auto is_excluded_namespace(std::meta::info ns) -> bool { + return ns == ^^::std || ns == ^^::boost; +} + +// True if the recursive scan of one of `namespaces` enters `ns` from above: +// `ns` is nested in one of them, and neither it nor a namespace in between is +// excluded. A listed namespace is entered whether it is excluded or not, so +// the question is asked of the ancestors of `ns` only, never of `ns` itself. +consteval auto scan_reaches( + std::meta::info ns, const std::vector& namespaces) + -> bool { + auto scope = ns; + + while (!is_excluded_namespace(scope) && std::meta::has_parent(scope)) { + scope = std::meta::parent_of(scope); + + if (contains(namespaces, scope)) { + return true; + } + } + + return false; +} + +// True if a scan of `namespaces` declares `type`: it enters the namespace +// that holds it, and finds it there, or in a class it walks into from there. +// Answered from the position of `type` alone, so that it costs the depth of +// its nesting, not a search of everything the scan found. What it says no to +// is found only by other means - through an alias that names it, in the base +// list of a class the scan does declare, or by being listed - or not at all. +consteval auto scan_declares( + std::meta::info type, const std::vector& namespaces) + -> bool { + auto scope = type; + + while (true) { + // A specialization of a class template is a member of no scope, and + // neither is anything nested in one: `members_of` yields the template. + if (std::meta::has_template_arguments(scope)) { + return false; + } + + // A local class has no parent. + if (!std::meta::has_parent(scope)) { + return false; + } + + scope = std::meta::parent_of(scope); + + if (std::meta::is_namespace(scope)) { + return contains(namespaces, scope) || + scan_reaches(scope, namespaces); + } + + // A class nested in a class: the scan walks into every class it + // declares, so keep climbing. + if (!std::meta::is_class_type(scope)) { + return false; + } + } +} + // ============================================================================= // base classes -// Append `type` and all the base classes transitively reachable from it to -// `types`, skipping the ones already present. Only public base specifiers are -// followed: a class reached solely through a private or protected base cannot -// take part in dispatch, because the conversion is not available to the -// library. A base the walk arrives at again is appended to `repeated` as well: -// it is a candidate for ambiguity, which `collect_dispatchable_bases` decides. -consteval void collect_reflected_bases( - std::meta::info type, std::vector& types, - std::vector& repeated) { - if (contains(types, type)) { - push_unique(repeated, type); +// A class reached by `walk_bases`. +struct base_record { + std::meta::info type; + // Arrived at by more than one public path: a candidate for ambiguity, + // which `is_dispatchable_base` decides. + bool repeated = false; + // A root, or a class with a public base that reaches one. + bool reaches_root = false; +}; + +consteval auto find(const std::vector& bases, std::meta::info type) + -> std::size_t { + for (std::size_t index = 0; index != bases.size(); ++index) { + if (bases[index].type == type) { + return index; + } + } + return bases.size(); +} + +// Mark the class at `index`, and every class above it, as repeated: the walk +// has arrived at it again, so it and its ancestors are reached through one +// more path than before. A class already marked has had its ancestors marked +// too, which bounds the work. +consteval void mark_repeated( + std::vector& bases, std::size_t index) { + if (bases[index].repeated) { return; } - types.push_back(type); + bases[index].repeated = true; // The range is held in a named local instead of being left to the // range-for to lifetime-extend, to work around GCC PR124645/PR124646. @@ -105,20 +193,69 @@ consteval void collect_reflected_bases( // object: the inner call destroys the vector the outer call is still // walking, and the loop fails with "accessing '' outside its // lifetime". A plain automatic variable is not an extended-ref temporary, - // so each frame gets its own. `scan_scope` below recurses too, and does - // the same. + // so each frame gets its own. Every recursive function here does the + // same. // // Fixed upstream in r16-8430. Drop this once no supported toolchain sits // in between - Ubuntu 26.04, which Boost.CI uses for the C++26 leg, ships // 16-20260322 (r16-8246) and does. - auto bases = + auto specifiers = std::meta::bases_of( + bases[index].type, std::meta::access_context::unchecked()); + + for (auto specifier : specifiers) { + if (std::meta::is_public(specifier)) { + // Every public base of a class the walk has been through is in + // `bases` already. + mark_repeated( + bases, + find(bases, std::meta::dealias(std::meta::type_of(specifier)))); + } + } +} + +// Append `type`, and every class reachable from it through public +// inheritance, to `bases` - each once, `type` first. Only public base +// specifiers are followed: a class reached solely through a private or +// protected base cannot take part in dispatch, because the conversion is not +// available to the library. Returns whether `type` reaches one of `roots`: it +// is one, or a public base of it does. +// +// A class the walk arrives at again is marked `repeated`, and so is everything +// above it, as `mark_repeated` explains. The mark is a suspicion, not a +// verdict: a virtual base is one subobject however many paths reach it. That +// is for `is_dispatchable_base` to decide, and it costs a template +// instantiation, so it is only asked about the classes marked here. A +// hierarchy without repeated inheritance instantiates nothing. +consteval auto walk_bases( + std::meta::info type, const std::vector& roots, + std::vector& bases) -> bool { + auto index = find(bases, type); + + if (index != bases.size()) { + mark_repeated(bases, index); + + return bases[index].reaches_root; + } + + bases.push_back({type, false, contains(roots, type)}); + + // A named local, for the reason given in `mark_repeated`. + auto specifiers = std::meta::bases_of(type, std::meta::access_context::unchecked()); - for (auto base : bases) { - if (std::meta::is_public(base)) { - collect_reflected_bases(std::meta::type_of(base), types, repeated); + for (auto specifier : specifiers) { + if (std::meta::is_public(specifier)) { + auto base = std::meta::dealias(std::meta::type_of(specifier)); + + // Walk every base, whatever the ones before said: the walk must + // be complete for `repeated` to mean anything. + if (walk_bases(base, roots, bases)) { + bases[index].reaches_root = true; + } } } + + return bases[index].reaches_root; } // True if a `derived` can be converted to a `base` - that is, if `base` is a @@ -132,139 +269,165 @@ consteval auto is_dispatchable_base( std::meta::add_pointer(derived), std::meta::add_pointer(base)); } -// Append to `types` the classes `type` can dispatch as: itself, and the base -// classes reachable from it through public inheritance, less the ones that -// repeated inheritance makes ambiguous. An ambiguous base cannot take part in -// dispatch - no reference to it can be formed - so it is left out. -// `use_classes` rejects such a hierarchy outright; `register_classes` cannot, -// because it sees classes it was never asked about. -// -// Only the bases the walk arrived at more than once can be ambiguous, and only -// those are put to `is_dispatchable_base`: a hierarchy without repeated -// inheritance instantiates nothing. -consteval void collect_dispatchable_bases( - std::meta::info type, std::vector& types) { - std::vector reachable, repeated; - collect_reflected_bases(type, reachable, repeated); - - for (auto base : reachable) { - if (contains(repeated, base) && !is_dispatchable_base(type, base)) { - continue; - } - - types.push_back(base); - } -} - // ============================================================================= // scan -// True if `member` is a namespace that a recursive scan does not enter: `std` -// and `boost`. Walking them would cost a great deal and find nothing: a method -// cannot be declared on a class the program has never heard of. The nested -// namespaces - `std::chrono`, `boost::mp11`, the inline versioning ones - are -// reached only through their parent, so they are left out with it. The -// exclusion applies only to recursion: a namespace listed explicitly is always -// scanned, which is how a class in `std` or `boost` is registered. -consteval auto is_excluded_namespace(std::meta::info member) -> bool { - auto ns = std::meta::dealias(member); - - return ns == ^^::std || ns == ^^::boost; -} - -// The class template specialization that `member` names: `member` itself, if it -// is a type - or an alias for one - that is a specialization; or the class that -// encloses `member`'s type, if `member` is a variable of a nested type. This is -// how a `method` is found: the core interface names it in an alias, and a -// registrar - the one `BOOST_OPENMETHOD_OVERRIDE` creates, or one written by -// hand - is a variable of type `method<...>::override<...>`. Returns an invalid -// reflection if `member` names no specialization. -consteval auto specialization_named_by(std::meta::info member) - -> std::meta::info { - if (std::meta::is_type(member)) { - auto type = std::meta::dealias(member); - - if (std::meta::has_template_arguments(type)) { - return type; - } - - return std::meta::info(); - } - - if (std::meta::is_variable(member)) { - auto enclosing = std::meta::type_of(member); - - if (std::meta::has_parent(enclosing)) { - auto parent = std::meta::parent_of(enclosing); +// What a scan of a set of namespaces yields, before any registry is looked at. +struct reflected_scan { + // The namespaces scanned - `^^::` when none was listed - and the classes + // listed alongside them, with cv-qualification removed. + std::vector namespaces; + std::vector listed; + // The specializations of the method template that the scanned scopes + // name, in a `using` declaration or as the type of a registrar object. + std::vector methods; + // The complete classes the scanned scopes declare, that have at least one + // base class. Each appears once, without a search: the scan walks a scope + // once, and a declaration belongs to one scope. A class with no base + // class is left out: it could only be registered as a root, and roots are + // not found by the scan. + std::vector declared; + // The classes an alias names that the scan does not declare - in a + // namespace it does not enter, or nested in a class template + // specialization - with the same restrictions. Searched, as a class may + // be aliased more than once. + std::vector aliased; +}; - if (std::meta::is_type(parent) && - std::meta::has_template_arguments(parent)) { - return parent; - } +// The specialization of `Template` that a variable `member` names, if it is a +// registrar: a variable of a type nested in the specialization, which is what +// `BOOST_OPENMETHOD_OVERRIDE` creates, and what a hand-written +// `method<...>::override<...>` is. Returns an invalid reflection otherwise. +consteval auto specialization_named_by_variable( + std::meta::info member, std::meta::info Template) -> std::meta::info { + auto enclosing = std::meta::type_of(member); + + if (std::meta::has_parent(enclosing)) { + auto parent = std::meta::parent_of(enclosing); + + if (std::meta::is_type(parent) && + std::meta::has_template_arguments(parent) && + std::meta::template_of(parent) == Template) { + return parent; } } return std::meta::info(); } +// True if `type`, a complete class, has at least one base class. +consteval auto has_bases(std::meta::info type) -> bool { + return !std::meta::bases_of(type, std::meta::access_context::unchecked()) + .empty(); +} + // Walk `scope` - a namespace, or a class - and the namespaces and classes -// nested in it, collecting the specializations of `Template` that its members -// name, and the complete class types they declare. Nothing else is retained: -// the scan of a large namespace must not build a list of everything in it. +// nested in it, filling in `scan`. Nothing else is retained: the scan of a +// large namespace must not build a list of everything in it. // -// A member that *names* a class registers it, which is how a class reached -// through an alias is found. Recursion is narrower: it enters only a class the -// scope actually *declares*, which `parent_of` answers. Following an alias -// instead would walk whatever it points at - a member `using` for -// `std::string` would drag the whole of `basic_string` in behind it - and -// `std` is excluded from the scan for that very reason. +// The scan never goes through an alias. A namespace alias may name an +// enclosing namespace, which would loop forever, or a namespace the scan +// stays out of; the namespace it names is reached from its parent anyway, or +// deliberately not. A type alias registers the class it names, but the scan +// does not walk into it either: a `using` for `std::string` would drag the +// whole of `basic_string` in behind it. Nor does it ask whether an aliased +// specialization of a class template is complete: the question instantiates +// the specialization, and a `using Edge = std::pair` over a `Node` +// defined elsewhere would fail to compile - in a translation unit that is +// valid C++17. A specialization is registered by being listed, by a method +// dispatching on it, or by sitting in the base list of a class the scan finds. consteval void scan_scope( - std::meta::info scope, std::meta::info Template, - std::vector& specializations, - std::vector& classes) { - // A named local, for the reason given in `collect_reflected_bases`. + std::meta::info scope, std::meta::info Template, reflected_scan& scan) { + // A named local, for the reason given in `mark_repeated`. auto members = std::meta::members_of(scope, std::meta::access_context::unchecked()); for (auto member : members) { if (std::meta::is_namespace(member)) { - if (!is_excluded_namespace(member)) { - scan_scope(member, Template, specializations, classes); + if (!std::meta::is_namespace_alias(member) && + !is_excluded_namespace(member)) { + scan_scope(member, Template, scan); } continue; } - auto specialization = specialization_named_by(member); + if (std::meta::is_type(member)) { + if (std::meta::is_type_alias(member)) { + // An alias may add cv-qualification - `using CDog = const + // Dog` - which is not a distinct class to register: the + // registry would hold `Dog` and `const Dog` as two lattice + // nodes, each with its own hash slot and dispatch table row. + auto type = std::meta::remove_cv(std::meta::dealias(member)); + + if (std::meta::has_template_arguments(type)) { + // A method is found through the alias `BOOST_OPENMETHOD` + // declares for it, or one written by hand. + if (std::meta::template_of(type) == Template) { + push_unique(scan.methods, type); + } + } else if ( + std::meta::is_class_type(type) && + std::meta::is_complete_type(type) && + !scan_declares(type, scan.namespaces) && has_bases(type)) { + push_unique(scan.aliased, type); + } - if (specialization != std::meta::info() && - std::meta::template_of(specialization) == Template) { - push_unique(specializations, specialization); - } + continue; + } - if (std::meta::is_type(member)) { - // An alias may add cv-qualification - - // `using CDog = const Dog` - which is not a distinct class to - // register: the registry would hold `Dog` and `const Dog` as two - // lattice nodes, each with its own hash slot and dispatch table - // row. - auto type = std::meta::remove_cv(std::meta::dealias(member)); - - if (std::meta::is_class_type(type) && - std::meta::is_complete_type(type)) { - auto known = contains(classes, type); - push_unique(classes, type); - - // The injected class name makes a class a member of itself, - // and a class already walked may be named again; `known` - // stops both. - if (!known && std::meta::has_parent(type) && - std::meta::parent_of(type) == scope) { - scan_scope(type, Template, specializations, classes); + // A class the scope declares. `parent_of` says so; a member class + // whose parent is elsewhere, such as one brought in by a + // using-declaration, is left to the scope that declares it. + if (std::meta::is_class_type(member) && + std::meta::is_complete_type(member) && + std::meta::has_parent(member) && + std::meta::parent_of(member) == scope) { + if (has_bases(member)) { + scan.declared.push_back(member); } + + scan_scope(member, Template, scan); } + + continue; } + + if (std::meta::is_variable(member)) { + auto specialization = + specialization_named_by_variable(member, Template); + + if (specialization != std::meta::info()) { + push_unique(scan.methods, specialization); + } + } + } +} + +// Scan `namespaces` - or the global namespace, if the list is empty - for +// the specializations of `Template` and the classes described in +// `reflected_scan`. A listed namespace nested in another listed one is +// reached by the recursion, and is not scanned again. +consteval auto scan_namespaces( + std::vector namespaces, + std::vector listed, std::meta::info Template) + -> reflected_scan { + reflected_scan scan; + + if (namespaces.empty()) { + namespaces.push_back(^^::); } + + scan.namespaces = namespaces; + scan.listed = listed; + + for (auto ns : namespaces) { + if (!scan_reaches(ns, namespaces)) { + scan_scope(ns, Template, scan); + } + } + + return scan; } } // namespace boost::openmethod::detail diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index c489c398..d0711781 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -608,8 +608,11 @@ inline constexpr bool method_not_found = false; //! the classes to register, and the registries; each group optional, in that //! order. With no namespace group, the global namespace is scanned. //! -//! Reflection sees only what precedes it, so this macro must come **after** the -//! declarations it is meant to find - at the bottom of the file: +//! The scan sees the whole translation unit on current compilers, but the +//! standard promises less: a class or a method it would select, declared after +//! the macro in the same translation unit, makes the program ill-formed, no +//! diagnostic required. Put the macro **after** the declarations it is meant to +//! find, at the bottom of the file: //! //! @code //! struct Animal { virtual ~Animal() = default; }; @@ -627,7 +630,7 @@ inline constexpr bool method_not_found = false; //! @endcode //! //! Without reflection - in C++17, or in C++26 without the compiler flag that -//! enables it - this macro expands to nothing, so a file that also calls +//! enables it - this macro expands to a no-op, so a file that also calls //! @ref BOOST_OPENMETHOD_CLASSES builds under either standard. //! //! @param ... Braced groups: namespaces, classes, registries - see above. diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index eb0ed323..17943524 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -177,11 +177,9 @@ struct not_initialized : openmethod_error { //! include:errors_missing_class_call.cpp#classes;use //! //! @note With a compiler that supports C++26 reflection, @ref -//! BOOST_OPENMETHOD_REGISTER_CLASSES registers these classes on its own, and the -//! examples above no longer report anything. The error remains reachable - for -//! a class in a namespace the scan does not cover, or in a registry with an -//! @ref boost::openmethod::policies::explicit_class_registration policy, which -//! is what the examples use. +//! BOOST_OPENMETHOD_REGISTER_CLASSES finds these classes on its own. The +//! examples do not call it, so they report the error under either standard; a +//! program that does reaches it only for a class the scan does not cover. //! //! @see [Error Handling](xref:ROOT:error_handling.adoc) struct missing_class : openmethod_error { @@ -220,11 +218,9 @@ struct missing_class : openmethod_error { //! include:errors_missing_class_call.cpp#fix //! //! @note With a compiler that supports C++26 reflection, @ref -//! BOOST_OPENMETHOD_REGISTER_CLASSES registers these classes on its own, and the -//! examples above no longer report anything. The error remains reachable - for -//! a class in a namespace the scan does not cover, or in a registry with an -//! @ref boost::openmethod::policies::explicit_class_registration policy, which -//! is what the examples use. +//! BOOST_OPENMETHOD_REGISTER_CLASSES finds these classes on its own. The +//! examples do not call it, so they report the error under either standard; a +//! program that does reaches it only for a class the scan does not cover. //! //! @see [Error Handling](xref:ROOT:error_handling.adoc) struct missing_base : openmethod_error { @@ -927,27 +923,6 @@ struct runtime_checks final { struct fn {}; }; -// ----------------------------------------------------------------------------- -// explicit_class_registration - -//! Policy to disable reflection-based class registration. -//! -//! When the compiler supports C++26 reflection, the library registers the -//! classes of virtual parameters, and their base classes, on its own; see @ref -//! use_classes. If this policy is present, it does not: every class must be -//! registered with @ref use_classes or @ref BOOST_OPENMETHOD_CLASSES, exactly -//! as in C++17. -//! -//! The policy has no effect if the compiler does not support reflection. -//! -//! @see [Registries and Policies](xref:ROOT:registries_and_policies.adoc) -struct explicit_class_registration final { - // Policy category. - using category = explicit_class_registration; - template - struct fn {}; -}; - } // namespace policies // ----------------------------------------------------------------------------- @@ -1396,14 +1371,6 @@ class registry : public detail::registry_base { //! `true` if the registry has an indirect_vptr policy. static constexpr auto has_indirect_vptr = !std::is_same_v, void>; - - //! `true` if the library registers classes by reflection. - //! - //! `true` if the compiler supports C++26 reflection and the registry does - //! not have an @ref policies::explicit_class_registration policy. - static constexpr auto has_reflected_class_registration = - BOOST_OPENMETHOD_HAS_REFLECTION && - std::is_same_v, void>; }; template diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6508edac..85954411 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -94,7 +94,6 @@ foreach(test_cpp ${test_cpp_files}) set(test_target "boost_openmethod-${test}") add_executable(${test_target} EXCLUDE_FROM_ALL ${test_cpp}) target_link_libraries(${test_target} PRIVATE Boost::openmethod Boost::unit_test_framework) - boost_openmethod_enable_reflection(${test_target}) boost_openmethod_add_test(${test_target}) add_dependencies(tests ${test_target}) @@ -128,7 +127,6 @@ endforeach() add_executable(boost_openmethod-test_mix_release_debug EXCLUDE_FROM_ALL mix_release_debug/main.cpp mix_release_debug/lib.cpp) target_link_libraries(boost_openmethod-test_mix_release_debug PRIVATE Boost::openmethod Boost::unit_test_framework) -boost_openmethod_enable_reflection(boost_openmethod-test_mix_release_debug) boost_openmethod_add_test(boost_openmethod-test_mix_release_debug) add_dependencies(tests boost_openmethod-test_mix_release_debug) @@ -160,7 +158,6 @@ set_property( function(openmethod_compile_fail_test testname fail_regex) set(test_target "boost_openmethod-${testname}") add_library(${test_target} STATIC EXCLUDE_FROM_ALL "${testname}.cpp") - boost_openmethod_enable_reflection(${test_target}) target_link_libraries(${test_target} PRIVATE Boost::openmethod) add_test( NAME "${test_target}" diff --git a/test/compile_fail_reflection_incomplete_class.cpp b/test/compile_fail_reflection_incomplete_class.cpp new file mode 100644 index 00000000..d5adcf83 --- /dev/null +++ b/test/compile_fail_reflection_incomplete_class.cpp @@ -0,0 +1,32 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: a listed class must be complete + +#include + +#if !BOOST_OPENMETHOD_HAS_REFLECTION + +// Without reflection there is nothing to check; produce the expected +// diagnostic so the test passes under any configuration. +#error a listed class must be complete + +#else + +namespace app { + +// Declared, never defined: a forward declaration left in the list by mistake. +struct Animal; + +BOOST_OPENMETHOD_REGISTER(boost::openmethod::register_classes<{^^app::Animal}>); + +} // namespace app + +#endif + +int main() { + return 0; +} diff --git a/test/dynamic_loading/main.cpp b/test/dynamic_loading/main.cpp index 6ca51418..8ff79d59 100644 --- a/test/dynamic_loading/main.cpp +++ b/test/dynamic_loading/main.cpp @@ -16,6 +16,7 @@ #include "registry.hpp" #include "method.hpp" +#include "../test_classes.hpp" #include #include @@ -29,7 +30,7 @@ using namespace boost::openmethod; using state_id_fn = const void*(); -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); boost::filesystem::path find_lib( const boost::filesystem::path& dir, const char* name_fragment) { @@ -148,3 +149,7 @@ BOOST_AUTO_TEST_CASE(test_shared_state) { BOOST_TEST(std::string(overrider_speak(cat)) == "meow"); } } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/dynamic_loading/method.cpp b/test/dynamic_loading/method.cpp index 350e081d..a4480989 100644 --- a/test/dynamic_loading/method.cpp +++ b/test/dynamic_loading/method.cpp @@ -10,6 +10,7 @@ #include "registry.hpp" #include "method.hpp" #include "shared_overrider.hpp" +#include "../test_classes.hpp" #include @@ -24,7 +25,7 @@ using namespace boost::openmethod; namespace mp11 = boost::mp11; -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "?"; @@ -60,3 +61,7 @@ BOOST_SYMBOL_EXPORT void method_call_meet( result = meet(a, b); } } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/dynamic_loading/overrider.cpp b/test/dynamic_loading/overrider.cpp index 15a9419f..e2af95b7 100644 --- a/test/dynamic_loading/overrider.cpp +++ b/test/dynamic_loading/overrider.cpp @@ -9,6 +9,7 @@ #include "method.hpp" #include "shared_overrider.hpp" +#include "../test_classes.hpp" #if defined(_WIN32) || defined(__CYGWIN__) #include @@ -29,7 +30,7 @@ BOOST_OPENMETHOD_OVERRIDE( return {"wag tails", next_word}; } -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); extern "C" { @@ -54,3 +55,7 @@ BOOST_SYMBOL_EXPORT void overrider_call_meet( result = meet(a, b); } } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/dynamic_loading/registry.cpp b/test/dynamic_loading/registry.cpp index 1f309866..174848f8 100644 --- a/test/dynamic_loading/registry.cpp +++ b/test/dynamic_loading/registry.cpp @@ -16,8 +16,13 @@ #include "registry.hpp" #include "classes.hpp" +#include "../test_classes.hpp" using namespace boost::openmethod; namespace mp11 = boost::mp11; -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/custom_registry/lib.cpp b/test/implicit_shared_libraries/custom_registry/lib.cpp index 29972142..813cec89 100644 --- a/test/implicit_shared_libraries/custom_registry/lib.cpp +++ b/test/implicit_shared_libraries/custom_registry/lib.cpp @@ -14,6 +14,7 @@ #define LIB_SOURCE #include "lib.hpp" +#include "../../test_classes.hpp" // The export is an explicit instantiation *definition*, so a program may // contain only one: it belongs in a .cpp, never in a header, and this is that @@ -24,7 +25,7 @@ BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(custom_registry); using namespace boost::openmethod; -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, Cow); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat, Cow); BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "?"; @@ -45,3 +46,7 @@ auto lib_speak(virtual_ptr animal) -> const char* { auto lib_make_dog() -> unique_virtual_ptr { return make_unique_virtual(); } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/custom_registry/lib2.cpp b/test/implicit_shared_libraries/custom_registry/lib2.cpp index 8ca23ac4..a68b726c 100644 --- a/test/implicit_shared_libraries/custom_registry/lib2.cpp +++ b/test/implicit_shared_libraries/custom_registry/lib2.cpp @@ -17,10 +17,11 @@ #define LIB_SOURCE #include "lib.hpp" +#include "../../test_classes.hpp" using namespace boost::openmethod; -BOOST_OPENMETHOD_CLASSES(Animal, Cow); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Cow); BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "moo"; @@ -29,3 +30,7 @@ BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { auto lib_make_cow() -> unique_virtual_ptr { return make_unique_virtual(); } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/custom_registry/main.cpp b/test/implicit_shared_libraries/custom_registry/main.cpp index 721034c0..b8e3b09c 100644 --- a/test/implicit_shared_libraries/custom_registry/main.cpp +++ b/test/implicit_shared_libraries/custom_registry/main.cpp @@ -10,6 +10,7 @@ // OWNS_REGISTRY_STATE is not defined here, so registry.hpp (via lib.hpp) // declares the state imported: this module uses the state the library owns. #include "lib.hpp" +#include "../../test_classes.hpp" #include @@ -17,7 +18,7 @@ using namespace boost::openmethod; -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, Cow); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat, Cow); // Registered by the executable; the library knows nothing about it. BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { @@ -61,3 +62,7 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { auto animal = make_unique_virtual(); BOOST_TEST(std::string(lib_speak(animal)) == "?"); } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/default_registry/lib.cpp b/test/implicit_shared_libraries/default_registry/lib.cpp index ea06a557..02f3ceb4 100644 --- a/test/implicit_shared_libraries/default_registry/lib.cpp +++ b/test/implicit_shared_libraries/default_registry/lib.cpp @@ -11,6 +11,7 @@ #define OWNS_REGISTRY_STATE #include "lib.hpp" +#include "../../test_classes.hpp" using namespace boost::openmethod; @@ -19,7 +20,7 @@ using namespace boost::openmethod; // error on GCC. BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry); -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "?"; @@ -45,3 +46,7 @@ auto lib_speak(virtual_ptr animal) -> const char* { auto lib_make_dog() -> unique_virtual_ptr { return make_unique_virtual(); } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/default_registry/main.cpp b/test/implicit_shared_libraries/default_registry/main.cpp index c134b641..35c6b2ce 100644 --- a/test/implicit_shared_libraries/default_registry/main.cpp +++ b/test/implicit_shared_libraries/default_registry/main.cpp @@ -8,6 +8,7 @@ #include #include "lib.hpp" +#include "../../test_classes.hpp" #include @@ -17,7 +18,7 @@ using namespace boost::openmethod; -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); // Registered by the executable; the library knows nothing about it. BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { @@ -80,3 +81,7 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { auto animal = make_unique_virtual(); BOOST_TEST(std::string(lib_speak(animal)) == "?"); } + +// Registers the classes above by reflection, when the compiler supports it. +// Last in the file: the standard wants the registrar after what it selects. +BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_capture_errors.hpp b/test/test_capture_errors.hpp index e1999a2e..a63cbc0c 100644 --- a/test/test_capture_errors.hpp +++ b/test/test_capture_errors.hpp @@ -11,13 +11,6 @@ // definition, the library include, and `test_registry` itself. Including it // first - before anything that pulls in core.hpp - is all a test has to do, // and there is no ordering left for a caller to get wrong. -// -// A test that is *about* a class the library must not find on its own - one -// that expects `missing_class` or `missing_base` - defines -// BOOST_OPENMETHOD_TEST_EXPLICIT_CLASS_REGISTRATION before including this -// header. The `explicit_class_registration` policy then leaves every -// registration to the test, exactly as in C++17, instead of letting reflection -// supply the class the test is withholding. struct test_registry; #define BOOST_OPENMETHOD_DEFAULT_REGISTRY test_registry @@ -36,15 +29,8 @@ struct capture_output : boost::openmethod::policies::output { }; }; -#ifdef BOOST_OPENMETHOD_TEST_EXPLICIT_CLASS_REGISTRATION -struct test_registry : - boost::openmethod::default_registry::with< - capture_output, - boost::openmethod::policies::explicit_class_registration> {}; -#else struct test_registry : boost::openmethod::default_registry::with {}; -#endif template struct capture_errors { diff --git a/test/test_checked_registry.hpp b/test/test_checked_registry.hpp index 657e5bd1..e29b132f 100644 --- a/test/test_checked_registry.hpp +++ b/test/test_checked_registry.hpp @@ -15,30 +15,15 @@ // // `runtime_checks` catches what initialize() cannot; `throw_error_handler` // turns the diagnosis into an exception the test can catch. -// -// A test that is *about* a class the library must not find on its own - one -// that expects `missing_class` or `missing_base` - defines -// BOOST_OPENMETHOD_TEST_EXPLICIT_CLASS_REGISTRATION before including this -// header. The `explicit_class_registration` policy then leaves every -// registration to the test, exactly as in C++17, instead of letting reflection -// supply the class the test is withholding. struct test_registry; #define BOOST_OPENMETHOD_DEFAULT_REGISTRY test_registry #include #include -#ifdef BOOST_OPENMETHOD_TEST_EXPLICIT_CLASS_REGISTRATION -struct test_registry : - boost::openmethod::default_registry::with< - boost::openmethod::policies::runtime_checks, - boost::openmethod::policies::throw_error_handler, - boost::openmethod::policies::explicit_class_registration> {}; -#else struct test_registry : boost::openmethod::default_registry::with< boost::openmethod::policies::runtime_checks, boost::openmethod::policies::throw_error_handler> {}; -#endif #endif diff --git a/test/test_class_registration_missing_base_class.cpp b/test/test_class_registration_missing_base_class.cpp index af4280f0..e1f395b9 100644 --- a/test/test_class_registration_missing_base_class.cpp +++ b/test/test_class_registration_missing_base_class.cpp @@ -3,10 +3,6 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) -// This test is *about* a class that is not registered, so the library must not -// register it by reflection. -#define BOOST_OPENMETHOD_TEST_EXPLICIT_CLASS_REGISTRATION - #include "test_checked_registry.hpp" #include diff --git a/test/test_class_registration_unknown_class_overrider.cpp b/test/test_class_registration_unknown_class_overrider.cpp index 61d868a3..0781b49f 100644 --- a/test/test_class_registration_unknown_class_overrider.cpp +++ b/test/test_class_registration_unknown_class_overrider.cpp @@ -3,10 +3,6 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) -// This test is *about* a class that is not registered, so the library must not -// register it by reflection. -#define BOOST_OPENMETHOD_TEST_EXPLICIT_CLASS_REGISTRATION - #include "test_checked_registry.hpp" #include diff --git a/test/test_classes.hpp b/test/test_classes.hpp index 8947c3c4..7b2b9081 100644 --- a/test/test_classes.hpp +++ b/test/test_classes.hpp @@ -17,13 +17,22 @@ //! classes go unregistered, and every test still has to pass. //! //! Tests that check what happens when a class is *not* registered keep -//! `BOOST_OPENMETHOD_CLASSES`, and put -//! `boost::openmethod::policies::explicit_class_registration` in their -//! registry so that the library leaves the registration to them. +//! `BOOST_OPENMETHOD_CLASSES`, and do not call +//! `BOOST_OPENMETHOD_REGISTER_CLASSES`: nothing is registered by reflection +//! unless that macro - or `register_classes` - is used, so the class they +//! withhold stays withheld under either standard. #if BOOST_OPENMETHOD_HAS_REFLECTION #define BOOST_OPENMETHOD_TEST_CLASSES(...) #else +// The build asked for reflection, but this translation unit does not have it: +// it would silently fall back to explicit registration, and the reflection +// tests - which reduce to a no-op without the feature - would pass without +// testing anything. CMake defines the symbol alongside the compiler options +// that enable reflection. +#ifdef BOOST_OPENMETHOD_EXPECT_REFLECTION +#error BOOST_OPENMETHOD_EXPECT_REFLECTION is defined, but the compiler does not provide C++26 reflection +#endif #define BOOST_OPENMETHOD_TEST_CLASSES(...) BOOST_OPENMETHOD_CLASSES(__VA_ARGS__) #endif diff --git a/test/test_custom_rtti_deferred.cpp b/test/test_custom_rtti_deferred.cpp index 365197f6..3f165ddd 100644 --- a/test/test_custom_rtti_deferred.cpp +++ b/test/test_custom_rtti_deferred.cpp @@ -237,5 +237,5 @@ BOOST_AUTO_TEST_CASE(custom_rtti_deferred) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_custom_rtti_simple.cpp b/test/test_custom_rtti_simple.cpp index 6009d1df..e0f23022 100644 --- a/test/test_custom_rtti_simple.cpp +++ b/test/test_custom_rtti_simple.cpp @@ -170,5 +170,5 @@ void call_poke(vptr a, std::ostream& os) { } // namespace using_vptr // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_custom_rtti_simple_projection.cpp b/test/test_custom_rtti_simple_projection.cpp index 96a2502c..53c8d73f 100644 --- a/test/test_custom_rtti_simple_projection.cpp +++ b/test/test_custom_rtti_simple_projection.cpp @@ -118,5 +118,5 @@ BOOST_AUTO_TEST_CASE(custom_rtti_simple_projection) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_custom_rtti_virtual_base.cpp b/test/test_custom_rtti_virtual_base.cpp index 0b000600..bf836951 100644 --- a/test/test_custom_rtti_virtual_base.cpp +++ b/test/test_custom_rtti_virtual_base.cpp @@ -195,5 +195,5 @@ void call_poke(vptr a, std::ostream& os) { } // namespace using_vptr // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_across_namespaces.cpp b/test/test_dispatch_across_namespaces.cpp index 9b1d234a..852abeeb 100644 --- a/test/test_dispatch_across_namespaces.cpp +++ b/test/test_dispatch_across_namespaces.cpp @@ -47,5 +47,5 @@ BOOST_AUTO_TEST_CASE(across_namespaces) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp index 7d696d2b..f8587e92 100644 --- a/test/test_dispatch_boost_any.cpp +++ b/test/test_dispatch_boost_any.cpp @@ -403,5 +403,5 @@ BOOST_AUTO_TEST_CASE(boost_any_class_in_hierarchy) { } // namespace BOOST_OPENMETHOD_GENSYM // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_comma_in_return_type.cpp b/test/test_dispatch_comma_in_return_type.cpp index 41d17dac..028f5e70 100644 --- a/test/test_dispatch_comma_in_return_type.cpp +++ b/test/test_dispatch_comma_in_return_type.cpp @@ -36,5 +36,5 @@ BOOST_AUTO_TEST_CASE(comma_in_return_type) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_intrusive_ptr.cpp b/test/test_dispatch_intrusive_ptr.cpp index 10900846..c0b4dbeb 100644 --- a/test/test_dispatch_intrusive_ptr.cpp +++ b/test/test_dispatch_intrusive_ptr.cpp @@ -170,5 +170,5 @@ BOOST_AUTO_TEST_CASE(intrusive_virtual_ptr_by_const_ref) { } // namespace BOOST_OPENMETHOD_GENSYM // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_lvalue_refs.cpp b/test/test_dispatch_lvalue_refs.cpp index 8db87ede..2beca7f7 100644 --- a/test/test_dispatch_lvalue_refs.cpp +++ b/test/test_dispatch_lvalue_refs.cpp @@ -39,5 +39,5 @@ BOOST_AUTO_TEST_CASE(cast_args_lvalue_refs) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_multi.cpp b/test/test_dispatch_multi.cpp index ac50be43..da2be109 100644 --- a/test/test_dispatch_multi.cpp +++ b/test/test_dispatch_multi.cpp @@ -74,5 +74,5 @@ BOOST_AUTO_TEST_CASE(simple) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_next_fn.cpp b/test/test_dispatch_next_fn.cpp index f488822f..e3fbaeff 100644 --- a/test/test_dispatch_next_fn.cpp +++ b/test/test_dispatch_next_fn.cpp @@ -53,5 +53,5 @@ BOOST_AUTO_TEST_CASE(test_next_fn) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_pointer.cpp b/test/test_dispatch_pointer.cpp index dc7b73cb..48821931 100644 --- a/test/test_dispatch_pointer.cpp +++ b/test/test_dispatch_pointer.cpp @@ -39,5 +39,5 @@ BOOST_AUTO_TEST_CASE(cast_args_pointer) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_rvalue_refs.cpp b/test/test_dispatch_rvalue_refs.cpp index f333b1be..20ab935c 100644 --- a/test/test_dispatch_rvalue_refs.cpp +++ b/test/test_dispatch_rvalue_refs.cpp @@ -47,5 +47,5 @@ BOOST_AUTO_TEST_CASE(cast_args_rvalue_refs) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_shared_ptr_by_ref.cpp b/test/test_dispatch_shared_ptr_by_ref.cpp index 00414140..36ecf122 100644 --- a/test/test_dispatch_shared_ptr_by_ref.cpp +++ b/test/test_dispatch_shared_ptr_by_ref.cpp @@ -44,5 +44,5 @@ BOOST_AUTO_TEST_CASE(cast_args_shared_ptr_by_ref) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_shared_ptr_by_value.cpp b/test/test_dispatch_shared_ptr_by_value.cpp index 69ec1173..7fc8a35b 100644 --- a/test/test_dispatch_shared_ptr_by_value.cpp +++ b/test/test_dispatch_shared_ptr_by_value.cpp @@ -41,5 +41,5 @@ BOOST_AUTO_TEST_CASE(cast_args_shared_ptr_by_value) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 45bd6925..5d2ca131 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -400,5 +400,5 @@ BOOST_AUTO_TEST_CASE(std_any_class_in_hierarchy) { } // namespace BOOST_OPENMETHOD_GENSYM // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_dispatch_unique_ptr.cpp b/test/test_dispatch_unique_ptr.cpp index 160a36de..f3141b16 100644 --- a/test/test_dispatch_unique_ptr.cpp +++ b/test/test_dispatch_unique_ptr.cpp @@ -43,5 +43,5 @@ BOOST_AUTO_TEST_CASE(cast_args_unique_ptr) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_n2216_covariant_return_type.cpp b/test/test_n2216_covariant_return_type.cpp index 1e1414c9..5878b6b6 100644 --- a/test/test_n2216_covariant_return_type.cpp +++ b/test/test_n2216_covariant_return_type.cpp @@ -49,5 +49,5 @@ BOOST_AUTO_TEST_CASE(covariant_return_type) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_n2216_pick_any_ambiguous.cpp b/test/test_n2216_pick_any_ambiguous.cpp index 5f9eb875..8eaa2df2 100644 --- a/test/test_n2216_pick_any_ambiguous.cpp +++ b/test/test_n2216_pick_any_ambiguous.cpp @@ -45,5 +45,5 @@ BOOST_AUTO_TEST_CASE(pick_any_ambiguous) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_namespaces.cpp b/test/test_namespaces.cpp index a1554810..024aa841 100644 --- a/test/test_namespaces.cpp +++ b/test/test_namespaces.cpp @@ -109,5 +109,5 @@ auto main() -> int { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_pointer_to_method.cpp b/test/test_pointer_to_method.cpp index 6ecd63db..94fde84b 100644 --- a/test/test_pointer_to_method.cpp +++ b/test/test_pointer_to_method.cpp @@ -39,5 +39,5 @@ BOOST_AUTO_TEST_CASE(noadl) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_reflection.cpp b/test/test_reflection.cpp index 2ae01b15..0b1e50d6 100644 --- a/test/test_reflection.cpp +++ b/test/test_reflection.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -30,6 +31,9 @@ BOOST_AUTO_TEST_CASE(reflection_not_supported) { #include #include +#include +#include +#include #include using namespace boost::openmethod; @@ -306,7 +310,9 @@ BOOST_AUTO_TEST_CASE(virtual_and_multiple_inheritance) { // // `use_classes` rejects an ambiguous base at compile time, because naming one // is a mistake in a hand-written list. Here the classes are collected -// mechanically, and a class that happens to have one must not break the build. +// mechanically, and a class that happens to have one must not break the build: +// it is registered under the bases it can be converted to, and the ambiguous +// one is left out of its list. namespace repeated_inheritance { @@ -348,15 +354,24 @@ BOOST_AUTO_TEST_CASE(repeated_inheritance_does_not_break_the_scan) { BOOST_TEST((registered(comp))); BOOST_TEST((registered(comp))); BOOST_TEST((registered(comp))); - // Animal is the only registered class Repeated derives from, and it is - // ambiguous, so Repeated is left with no base to dispatch under and is not - // registered at all. - BOOST_TEST((!registered(comp))); + // Registered, under Left and Right: Animal is ambiguous, and left out. + BOOST_TEST((registered(comp))); + + auto repeated = comp.class_map.at( + test_registry::rtti::type_index( + test_registry::rtti::static_type())); + BOOST_TEST(repeated->direct_bases.size() == 2u); Dog dog; Left left; BOOST_TEST(poke(dog) == "bark"); BOOST_TEST(poke(left) == "generic"); + + // A Repeated cannot be passed as an Animal, but a Left can, and its dynamic + // type is what dispatch looks at. + Repeated repeated_obj; + Left& left_side = repeated_obj; + BOOST_TEST(poke(left_side) == "generic"); } // ============================================================================= @@ -520,11 +535,12 @@ BOOST_AUTO_TEST_CASE(a_base_another_method_dispatches_on_is_registered) { } // ============================================================================= -// The recorded bases are the direct ones +// The recorded bases are every registered ancestor // -// Reflection knows a class' direct bases, so the registry records those, not the -// whole ancestry. `initialize` derives the lattice from them either way; the -// point is to not ship, instantiate and store what it can work out for itself. +// Not the direct bases only: `initialize` derives those from the closure of +// what the records say, and a record that carries the whole ancestry stands on +// its own, whatever another registration - over other namespaces, in another +// translation unit - records alongside it. namespace direct_bases { @@ -553,22 +569,23 @@ BOOST_OPENMETHOD_OVERRIDE(poke, (C&), std::string) { BOOST_OPENMETHOD_REGISTER( register_classes<{^^direct_bases}, {^^direct_bases::test_registry}>); -BOOST_AUTO_TEST_CASE(recorded_bases_are_direct) { +BOOST_AUTO_TEST_CASE(recorded_bases_are_every_registered_ancestor) { using namespace direct_bases; auto comp = initialize(); // Each class_info names the class itself, as its own improper base, plus - // its direct bases - four entries for the whole chain, not ten. + // every registered class above it. std::size_t recorded = 0; for (auto& ci : test_registry::state().classes) { recorded += ci.last_base - ci.first_base; } - BOOST_TEST(recorded == 7u); // A: 1, B/C/D: 2 each + BOOST_TEST(recorded == 10u); // A: 1, B: 2, C: 3, D: 4 - // The lattice initialize derives from them is still the full chain. + // The lattice initialize derives from them is the chain, with the direct + // bases worked out. auto a = comp.class_map.at( test_registry::rtti::type_index(test_registry::rtti::static_type())); auto d = comp.class_map.at( @@ -588,39 +605,6 @@ BOOST_AUTO_TEST_CASE(recorded_bases_are_direct) { BOOST_TEST(poke(d_obj) == "C"); } -// ============================================================================= -// explicit_class_registration opts out - -namespace opted_out { - -struct test_registry : - test_registry_< - __COUNTER__, policies::explicit_class_registration, - policies::throw_error_handler> {}; - -struct Animal { - virtual ~Animal() = default; -}; - -struct Dog : Animal {}; - -BOOST_OPENMETHOD(poke, (virtual_), void, test_registry); - -BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), void) { -} - -} // namespace opted_out - -BOOST_OPENMETHOD_REGISTER( - register_classes<{^^opted_out}, {^^opted_out::test_registry}>); - -BOOST_AUTO_TEST_CASE(explicit_class_registration_disables_the_scan) { - static_assert(!opted_out::test_registry::has_reflected_class_registration); - // Nothing was registered, so initialize cannot resolve the method's - // virtual parameter. - BOOST_CHECK_THROW(initialize(), missing_class); -} - // ============================================================================= // Several namespaces in one registration @@ -868,6 +852,10 @@ struct Stray : boost_gate::Animal {}; } // namespace boost::om_reflection_test +// An alias, at global scope, to the very namespace the scan stays out of. The +// scan does not go through aliases, so this one changes nothing. +namespace om_reflection_test_alias = boost::om_reflection_test; + // The default scan of `^^::` recurses everywhere except into `std` and // `boost`, so it does not reach `Stray`. BOOST_OPENMETHOD_REGISTER(register_classes<{^^boost_gate::default_registry_}>); @@ -945,8 +933,8 @@ struct Animal { struct Dog : Animal {}; -// No registry group, so the classes go to BOOST_OPENMETHOD_DEFAULT_REGISTRY; -// no namespace group either, so nothing is scanned. +// No registry group, so the classes go to the default registry; no namespace +// group either, so the global namespace is scanned, as always. BOOST_OPENMETHOD_REGISTER( register_classes<{ ^^default_registry_target::Animal, ^^default_registry_target::Dog}>); @@ -988,10 +976,10 @@ struct Kennel { } }; -// A class the scope only *names* is registered, but not descended into: Alias -// adds nothing, and Kennel::Dog is found once. An alias that adds cv- -// qualification names the same class too, not a second one to give a lattice -// node, a hash slot and a dispatch table row of its own. +// An alias to a class the scan declares anyway adds nothing, and is not walked +// into: Kennel::Dog is found once. An alias that adds cv-qualification names +// the same class too, not a second one to give a lattice node, a hash slot and +// a dispatch table row of its own. using Alias = Kennel; using ConstDog = const Kennel::Dog; @@ -1026,4 +1014,405 @@ BOOST_AUTO_TEST_CASE(classes_nested_in_classes_are_found) { BOOST_TEST(poke(*Kennel::make_puppy()) == "bark"); } +// ============================================================================= +// Namespace aliases are not followed + +namespace namespace_aliases { + +struct test_registry : test_registry_<__COUNTER__> {}; + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; + +namespace detail { + +// An alias to the enclosing namespace: a scan that followed it would never +// end. +namespace up = ::namespace_aliases; + +} // namespace detail + +BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), std::string) { + return "bark"; +} + +} // namespace namespace_aliases + +BOOST_OPENMETHOD_REGISTER( + register_classes< + {^^namespace_aliases}, {^^namespace_aliases::test_registry}>); + +BOOST_AUTO_TEST_CASE(namespace_aliases_are_not_followed) { + using namespace namespace_aliases; + + auto comp = initialize(); + + BOOST_TEST((registered(comp))); + BOOST_TEST((registered(comp))); + + Dog dog; + BOOST_TEST(poke(dog) == "bark"); +} + +// ============================================================================= +// An alias scanned before the declaration does not hide nested classes +// +// `early` is scanned first, and names `Kennel`. That must not stop the scan +// from walking into `Kennel` when it reaches `late`, which declares it. + +namespace alias_first { + +struct test_registry : test_registry_<__COUNTER__> {}; + +namespace late { +struct Kennel; +} + +namespace early { +using K = late::Kennel; +} + +namespace late { + +struct Animal { + virtual ~Animal() = default; +}; + +struct Kennel { + struct Dog : Animal {}; +}; + +BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Kennel::Dog&), std::string) { + return "bark"; +} + +} // namespace late + +} // namespace alias_first + +BOOST_OPENMETHOD_REGISTER( + register_classes< + {^^alias_first::early, ^^alias_first::late}, + {^^alias_first::test_registry}>); + +BOOST_AUTO_TEST_CASE(an_alias_scanned_first_does_not_hide_nested_classes) { + using namespace alias_first; + + auto comp = initialize(); + + BOOST_TEST((registered(comp))); + + late::Kennel::Dog dog; + BOOST_TEST(late::poke(dog) == "bark"); +} + +// ============================================================================= +// A class between a registered class and a root is registered wherever it is +// +// Two registrations, each scanning its own namespaces - as a library and a +// plugin would, from their own translation units. The plugin's class derives +// from a class the plugin's scan never sees. Its record must carry it all the +// same, and the class must be registered, or the library's overrider for it +// would not apply to the plugin's class. + +namespace split { + +struct test_registry : test_registry_<__COUNTER__> {}; + +namespace core { + +struct Animal { + virtual ~Animal() = default; +}; + +BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry); +BOOST_OPENMETHOD(name, (virtual_), std::string, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Animal&), std::string) { + return "generic"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (Animal&), std::string) { + return "animal"; +} + +} // namespace core + +namespace lib { + +struct Dog : core::Animal {}; + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), std::string) { + return "bark"; +} + +} // namespace lib + +namespace plugin { + +struct Bulldog : lib::Dog {}; + +// The plugin overrides `name` only. Its registrar is what leads the plugin's +// scan to the method, and to `Animal` as a root. +BOOST_OPENMETHOD_OVERRIDE(name, (Bulldog&), std::string) { + return "bulldog"; +} + +} // namespace plugin + +} // namespace split + +BOOST_OPENMETHOD_REGISTER( + register_classes<{^^split::core, ^^split::lib}, {^^split::test_registry}>); +BOOST_OPENMETHOD_REGISTER( + register_classes<{^^split::plugin}, {^^split::test_registry}>); + +BOOST_AUTO_TEST_CASE(a_base_outside_the_scan_is_registered) { + using namespace split; + + auto comp = initialize(); + + BOOST_TEST((registered(comp))); + + auto bulldog = comp.class_map.at( + test_registry::rtti::type_index( + test_registry::rtti::static_type())); + BOOST_TEST(bulldog->direct_bases.size() == 1u); + BOOST_TEST(bulldog->transitive_bases.size() == 2u); + + plugin::Bulldog bulldog_obj; + // Dog's overrider, not Animal's: the edge to Dog survived. + BOOST_TEST(core::poke(bulldog_obj) == "bark"); + BOOST_TEST(core::name(bulldog_obj) == "bulldog"); +} + +// ============================================================================= +// A method's return type is not a root +// +// `stream` returns a class reference, and its overrider a reference to a +// derived class - a covariant return type, both in a namespace the scan does +// not enter. Were `std::ostream` a root, `initialize` would want +// `std::ostringstream` registered as well, and abort. `clone` shows the +// covariant check still armed when the return classes are in the scan. + +namespace return_types { + +struct test_registry : test_registry_<__COUNTER__> {}; + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; + +BOOST_OPENMETHOD(stream, (virtual_), std::ostream&, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(stream, (Dog&), std::ostringstream&) { + static std::ostringstream os; + return os; +} + +BOOST_OPENMETHOD(clone, (virtual_), Animal*, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(clone, (Dog & dog), Dog*) { + return &dog; +} + +} // namespace return_types + +BOOST_OPENMETHOD_REGISTER( + register_classes<{^^return_types}, {^^return_types::test_registry}>); + +BOOST_AUTO_TEST_CASE(a_return_type_is_not_a_root) { + using namespace return_types; + + auto comp = initialize(); + + BOOST_TEST((registered(comp))); + BOOST_TEST((registered(comp))); + BOOST_TEST((!registered(comp))); + + Dog dog; + stream(dog) << "bark"; + BOOST_TEST(static_cast(stream(dog)).str() == "bark"); + BOOST_TEST(clone(dog) == &dog); +} + +// ============================================================================= +// A specialization of a class template between a class and a root +// +// `members_of` yields the template, not its specializations, so the scan cannot +// find `Pet` on its own. It is in the base list of `Dog`, and that is how +// it is registered. + +namespace template_bases { + +struct test_registry : test_registry_<__COUNTER__> {}; + +struct Animal { + virtual ~Animal() = default; +}; + +template +struct Pet : Animal {}; + +struct Dog : Pet {}; + +BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Animal&), std::string) { + return "generic"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (Pet&), std::string) { + return "pet"; +} + +} // namespace template_bases + +BOOST_OPENMETHOD_REGISTER( + register_classes<{^^template_bases}, {^^template_bases::test_registry}>); + +BOOST_AUTO_TEST_CASE(a_specialization_in_a_base_list_is_registered) { + using namespace template_bases; + + auto comp = initialize(); + + BOOST_TEST((registered, test_registry>(comp))); + BOOST_TEST((registered(comp))); + + Dog dog; + BOOST_TEST(poke(dog) == "pet"); +} + +// ============================================================================= +// An alias to a specialization over an incomplete class +// +// Asking whether `Edge` or `NodeBox` is a complete type would instantiate the +// specialization, which fails: `Node` is never defined. The scan does not ask. + +namespace incomplete_aliases { + +struct test_registry : test_registry_<__COUNTER__> {}; + +struct Node; + +using Edge = std::pair; + +template +struct Box { + T value; +}; + +using NodeBox = Box; + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; + +BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), std::string) { + return "bark"; +} + +} // namespace incomplete_aliases + +BOOST_OPENMETHOD_REGISTER( + register_classes< + {^^incomplete_aliases}, {^^incomplete_aliases::test_registry}>); + +BOOST_AUTO_TEST_CASE(an_alias_to_a_specialization_is_not_instantiated) { + using namespace incomplete_aliases; + + auto comp = initialize(); + + BOOST_TEST((registered(comp))); + + Dog dog; + BOOST_TEST(poke(dog) == "bark"); +} + +// ============================================================================= +// Deep and wide hierarchies stay within the compiler's budget +// +// A chain of sixty classes, and two hundred classes deriving from one root, +// under the default -fconstexpr-ops-limit. The cost of the scan and of the +// records must be linear in the number of classes, give or take the depth. + +namespace budget { + +struct test_registry : test_registry_<__COUNTER__> {}; + +struct Animal { + virtual ~Animal() = default; +}; + +using C0 = Animal; + +#define BOOST_OPENMETHOD_TEST_CHAIN_LINK(z, n, _) \ + struct BOOST_PP_CAT(C, BOOST_PP_INC(n)) : BOOST_PP_CAT(C, n) {}; + +BOOST_PP_REPEAT(60, BOOST_OPENMETHOD_TEST_CHAIN_LINK, _) + +#undef BOOST_OPENMETHOD_TEST_CHAIN_LINK + +#define BOOST_OPENMETHOD_TEST_WIDE_LEAF(z, n, _) \ + struct BOOST_PP_CAT(W, n) : Animal {}; + +BOOST_PP_REPEAT(200, BOOST_OPENMETHOD_TEST_WIDE_LEAF, _) + +#undef BOOST_OPENMETHOD_TEST_WIDE_LEAF + +BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Animal&), std::string) { + return "generic"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (C30&), std::string) { + return "C30"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (W199&), std::string) { + return "W199"; +} + +} // namespace budget + +BOOST_OPENMETHOD_REGISTER( + register_classes<{^^budget}, {^^budget::test_registry}>); + +BOOST_AUTO_TEST_CASE(deep_and_wide_hierarchies_are_within_budget) { + using namespace budget; + + auto comp = initialize(); + + BOOST_TEST((registered(comp))); + BOOST_TEST((registered(comp))); + BOOST_TEST((registered(comp))); + + auto c60 = comp.class_map.at( + test_registry::rtti::type_index( + test_registry::rtti::static_type())); + BOOST_TEST(c60->direct_bases.size() == 1u); + BOOST_TEST(c60->transitive_bases.size() == 60u); + + C60 c60_obj; + C29 c29; + W199 w199; + BOOST_TEST(poke(c60_obj) == "C30"); + BOOST_TEST(poke(c29) == "generic"); + BOOST_TEST(poke(w199) == "W199"); +} + #endif diff --git a/test/test_rolex.cpp b/test/test_rolex.cpp index a61583f8..9a49f2cd 100644 --- a/test/test_rolex.cpp +++ b/test/test_rolex.cpp @@ -188,5 +188,5 @@ BOOST_AUTO_TEST_CASE(approve_via_wrapper) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_bad_call.cpp b/test/test_runtime_errors_bad_call.cpp index 611eabcf..6ab2cce5 100644 --- a/test/test_runtime_errors_bad_call.cpp +++ b/test/test_runtime_errors_bad_call.cpp @@ -55,5 +55,5 @@ BOOST_AUTO_TEST_CASE(bad_calls) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_bad_call_type_ids.cpp b/test/test_runtime_errors_bad_call_type_ids.cpp index 7388fe3b..fedcac7f 100644 --- a/test/test_runtime_errors_bad_call_type_ids.cpp +++ b/test/test_runtime_errors_bad_call_type_ids.cpp @@ -50,5 +50,5 @@ BOOST_AUTO_TEST_CASE(bad_call_type_ids) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp b/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp index 1d240740..ff83bb95 100644 --- a/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp +++ b/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp @@ -43,5 +43,5 @@ BOOST_AUTO_TEST_CASE(bad_call_type_ids_smart_ptr) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_duplicate_overrider.cpp b/test/test_runtime_errors_duplicate_overrider.cpp index ce05a300..98e87f8e 100644 --- a/test/test_runtime_errors_duplicate_overrider.cpp +++ b/test/test_runtime_errors_duplicate_overrider.cpp @@ -62,5 +62,5 @@ BOOST_AUTO_TEST_CASE(duplicate_overrider_is_ambiguous) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_initialize_unknown_class.cpp b/test/test_runtime_errors_initialize_unknown_class.cpp index cb3e4f92..1dc50856 100644 --- a/test/test_runtime_errors_initialize_unknown_class.cpp +++ b/test/test_runtime_errors_initialize_unknown_class.cpp @@ -3,10 +3,6 @@ // See accompanying file LICENSE_1_0.txt // or copy at http://www.boost.org/LICENSE_1_0.txt) -// This test is *about* a class that is not registered, so the library must not -// register it by reflection. -#define BOOST_OPENMETHOD_TEST_EXPLICIT_CLASS_REGISTRATION - #include "test_capture_errors.hpp" #include diff --git a/test/test_runtime_errors_throw_error.cpp b/test/test_runtime_errors_throw_error.cpp index a23e7c13..6e2ef5ab 100644 --- a/test/test_runtime_errors_throw_error.cpp +++ b/test/test_runtime_errors_throw_error.cpp @@ -43,5 +43,5 @@ BOOST_AUTO_TEST_CASE(throw_error) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_smart_virtual_ptr_value_semantics.cpp b/test/test_smart_virtual_ptr_value_semantics.cpp index 705de6a8..6bf464ff 100644 --- a/test/test_smart_virtual_ptr_value_semantics.cpp +++ b/test/test_smart_virtual_ptr_value_semantics.cpp @@ -445,5 +445,5 @@ template struct check_illegal_smart_ops< boost::intrusive_ptr, std::unique_ptr, direct_vector>; // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_static_rtti.cpp b/test/test_static_rtti.cpp index df35c345..ccdf4706 100644 --- a/test/test_static_rtti.cpp +++ b/test/test_static_rtti.cpp @@ -58,5 +58,5 @@ BOOST_AUTO_TEST_CASE(static_rtti) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_virtual_any_boost.cpp b/test/test_virtual_any_boost.cpp index 243c7a35..0fad5bce 100644 --- a/test/test_virtual_any_boost.cpp +++ b/test/test_virtual_any_boost.cpp @@ -338,5 +338,5 @@ BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { } // namespace BOOST_OPENMETHOD_GENSYM // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_virtual_any_std.cpp b/test/test_virtual_any_std.cpp index d2c7a1f4..d2ce6a3b 100644 --- a/test/test_virtual_any_std.cpp +++ b/test/test_virtual_any_std.cpp @@ -338,5 +338,5 @@ BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { } // namespace BOOST_OPENMETHOD_GENSYM // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_by_ref.cpp b/test/test_virtual_ptr_by_ref.cpp index ffdc8a04..a6f64646 100644 --- a/test/test_virtual_ptr_by_ref.cpp +++ b/test/test_virtual_ptr_by_ref.cpp @@ -64,5 +64,5 @@ BOOST_AUTO_TEST_CASE(test_virtual_ptr_by_ref) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_non_polymorphic.cpp b/test/test_virtual_ptr_non_polymorphic.cpp index e9a3e877..5aa93dd4 100644 --- a/test/test_virtual_ptr_non_polymorphic.cpp +++ b/test/test_virtual_ptr_non_polymorphic.cpp @@ -41,5 +41,5 @@ BOOST_AUTO_TEST_CASE(test_virtual_ptr_non_polymorphic) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_shared_by_const_ref.cpp b/test/test_virtual_ptr_shared_by_const_ref.cpp index b869b36f..3801aca4 100644 --- a/test/test_virtual_ptr_shared_by_const_ref.cpp +++ b/test/test_virtual_ptr_shared_by_const_ref.cpp @@ -46,5 +46,5 @@ BOOST_AUTO_TEST_CASE(test_virtual_shared_by_const_reference) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_shared_by_value.cpp b/test/test_virtual_ptr_shared_by_value.cpp index e90e52d9..fa455061 100644 --- a/test/test_virtual_ptr_shared_by_value.cpp +++ b/test/test_virtual_ptr_shared_by_value.cpp @@ -44,5 +44,5 @@ BOOST_AUTO_TEST_CASE(test_virtual_shared_by_value) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_unique.cpp b/test/test_virtual_ptr_unique.cpp index 09efee93..dcbf8482 100644 --- a/test/test_virtual_ptr_unique.cpp +++ b/test/test_virtual_ptr_unique.cpp @@ -44,5 +44,5 @@ BOOST_AUTO_TEST_CASE(test_virtual_unique) { } // Registers the classes above by reflection, when the compiler supports it. -// Must come last: reflection sees only what precedes it. +// Last in the file: the standard wants the registrar after what it selects. BOOST_OPENMETHOD_REGISTER_CLASSES(); From 02edcaa05c87d98b826611684242dde07ce79587 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 4 Sep 2026 14:53:19 -0400 Subject: [PATCH 2/3] doc: put the C++26 requirement of register_classes up front The register_classes reference mentioned the requirement in its title suffix and in a sentence at the very end of a long description. A WARNING admonition now opens the description, on the class page and on the BOOST_OPENMETHOD_REGISTER_CLASSES page, which folds in its "expands to a no-op" paragraph. @attention was the first choice; MrDocs drops it silently, paragraph and all. CLAUDE.md's list of doc-comment markup traps gains that one. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JDLWpTRETW5kfEgK9b2fgF --- CLAUDE.md | 7 +++++-- include/boost/openmethod/core.hpp | 10 +++++++--- include/boost/openmethod/macros.hpp | 10 ++++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3c8cc638..21ef8315 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -356,8 +356,8 @@ literal `href="#reference:.adoc"`. ### Doc-comment markup traps -MrDocs parses `//!` comments as Markdown plus Doxygen commands, then emits AsciiDoc. Three shapes -mis-render silently; all three were found by rendering, none by reading the source: +MrDocs parses `//!` comments as Markdown plus Doxygen commands, then emits AsciiDoc. Four shapes +mis-render silently; all four were found by rendering, none by reading the source: - **A line starting with `- ` becomes a list item.** House style uses ` - ` as an em-dash, which is fine mid-line but starts a stray bullet at the head of one. Rewrap so the dash never begins a @@ -367,6 +367,9 @@ mis-render silently; all three were found by rendering, none by reading the sour - **An inline `` `^^::` `` loses both carets** and renders as `::`. Escape the first one - `` `\^^::` `` - which comes through as `^^::`. Only this spelling is affected; `` `^^app` `` and `^^::` inside an `@code` block are fine. +- **`@attention` is dropped silently**, paragraph and all: no admonition, no text, no warning. + MrDocs knows `@note` and `@warning`, which render as NOTE and WARNING blocks wherever they sit in + the description - the first paragraph after the brief included. Use one of those. An `xref:reference:.adoc` path works only for macros, which MrDocs puts at the top level. A namespace-scoped symbol lives under `reference/boost/openmethod/`, so link it with diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index a826911a..39b2a530 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -3205,6 +3205,13 @@ class register_classes { //! Find the classes taking part in dispatch by reflection, and register them //! (C++26 and above). //! +//! @warning Requires C++26 reflection (P2996): a compiler that implements +//! it, and the flag that turns it on - `-freflection` with GCC 16. Without it +//! this class template does not exist: `BOOST_OPENMETHOD_HAS_REFLECTION` is 0, +//! and classes are registered with @ref use_classes or +//! @ref BOOST_OPENMETHOD_CLASSES. @ref BOOST_OPENMETHOD_REGISTER_CLASSES wraps +//! this class in a macro that expands to a no-op under C++17. +//! //! `register_classes` is a registrar class that finds the classes taking part //! in dispatch by reflection, and adds them to one or more registries. It makes //! @ref use_classes unnecessary in most cases. @@ -3286,9 +3293,6 @@ class register_classes { //! it can be converted to, is left out of its list. A class with no registered //! class above it is not registered at all. //! -//! This class template is available only if the compiler supports C++26 -//! reflection, i.e. if `BOOST_OPENMETHOD_HAS_REFLECTION` is 1. -//! //! @tparam Groups Braced groups of reflections: namespaces, classes, //! registries, in that order. //! diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp index d0711781..cf536e9d 100644 --- a/include/boost/openmethod/macros.hpp +++ b/include/boost/openmethod/macros.hpp @@ -599,6 +599,12 @@ inline constexpr bool method_not_found = false; #else //! Find the classes taking part in dispatch by reflection, and register them. //! +//! @warning Does its work only with C++26 reflection (P2996): a compiler +//! that implements it, and the flag that turns it on - `-freflection` with +//! GCC 16. Without it - in C++17, or in C++26 without the flag - the macro +//! expands to a no-op, so a file that also calls @ref BOOST_OPENMETHOD_CLASSES +//! builds under either standard. +//! //! It makes @ref BOOST_OPENMETHOD_CLASSES unnecessary in most cases. //! //! This macro is a wrapper around @ref boost::openmethod::register_classes; see @@ -629,10 +635,6 @@ inline constexpr bool method_not_found = false; //! BOOST_OPENMETHOD_REGISTER_CLASSES(); // registers all four classes //! @endcode //! -//! Without reflection - in C++17, or in C++26 without the compiler flag that -//! enables it - this macro expands to a no-op, so a file that also calls -//! @ref BOOST_OPENMETHOD_CLASSES builds under either standard. -//! //! @param ... Braced groups: namespaces, classes, registries - see above. //! //! @see [Methods and Overriders](xref:ROOT:basics.adoc) From e50e1e1897f2771ecd8f08931ea1d28ea3e79093 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 5 Sep 2026 10:23:32 -0400 Subject: [PATCH 3/3] reflection: tidy the leftovers from the review Four small items the review listed below its cap: - CLAUDE.md's list of CMake options gains BOOST_OPENMETHOD_ENABLE_REFLECTION. - reflection_group::size mirrors the Size template parameter as a static constexpr member, instead of a data member the constructor set to the same value. - The three-line trailer pasted into 45 tests - the reflection registration and the comment explaining why it comes last - is one macro, BOOST_OPENMETHOD_TEST_REGISTER_CLASSES, in test_classes.hpp, which carries the explanation once. - The CMake reflection probe compiles config/has_reflection.cpp, the file b2's probe uses, instead of a second copy of it embedded as a string; as a static library, since the file has no main. The CMP0067 save-and-restore around the probe covers the target type too. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JDLWpTRETW5kfEgK9b2fgF --- CLAUDE.md | 1 + CMakeLists.txt | 57 +++++++------------ .../boost/openmethod/detail/reflection.hpp | 5 +- test/dynamic_loading/main.cpp | 4 +- test/dynamic_loading/method.cpp | 4 +- test/dynamic_loading/overrider.cpp | 4 +- test/dynamic_loading/registry.cpp | 4 +- .../custom_registry/lib.cpp | 4 +- .../custom_registry/lib2.cpp | 4 +- .../custom_registry/main.cpp | 4 +- .../default_registry/lib.cpp | 4 +- .../default_registry/main.cpp | 4 +- test/test_classes.hpp | 10 ++++ test/test_custom_rtti_deferred.cpp | 4 +- test/test_custom_rtti_simple.cpp | 4 +- test/test_custom_rtti_simple_projection.cpp | 4 +- test/test_custom_rtti_virtual_base.cpp | 4 +- test/test_dispatch_across_namespaces.cpp | 4 +- test/test_dispatch_boost_any.cpp | 4 +- test/test_dispatch_comma_in_return_type.cpp | 4 +- test/test_dispatch_intrusive_ptr.cpp | 4 +- test/test_dispatch_lvalue_refs.cpp | 4 +- test/test_dispatch_multi.cpp | 4 +- test/test_dispatch_next_fn.cpp | 4 +- test/test_dispatch_pointer.cpp | 4 +- test/test_dispatch_rvalue_refs.cpp | 4 +- test/test_dispatch_shared_ptr_by_ref.cpp | 4 +- test/test_dispatch_shared_ptr_by_value.cpp | 4 +- test/test_dispatch_std_any.cpp | 4 +- test/test_dispatch_unique_ptr.cpp | 4 +- test/test_n2216_covariant_return_type.cpp | 4 +- test/test_n2216_pick_any_ambiguous.cpp | 4 +- test/test_namespaces.cpp | 4 +- test/test_pointer_to_method.cpp | 4 +- test/test_rolex.cpp | 4 +- test/test_runtime_errors_bad_call.cpp | 4 +- .../test_runtime_errors_bad_call_type_ids.cpp | 4 +- ...ime_errors_bad_call_type_ids_smart_ptr.cpp | 4 +- ...est_runtime_errors_duplicate_overrider.cpp | 4 +- test/test_runtime_errors_throw_error.cpp | 4 +- ...test_smart_virtual_ptr_value_semantics.cpp | 6 +- test/test_static_rtti.cpp | 4 +- test/test_virtual_any_boost.cpp | 4 +- test/test_virtual_any_std.cpp | 4 +- test/test_virtual_ptr_by_ref.cpp | 4 +- test/test_virtual_ptr_non_polymorphic.cpp | 4 +- test/test_virtual_ptr_shared_by_const_ref.cpp | 4 +- test/test_virtual_ptr_shared_by_value.cpp | 4 +- test/test_virtual_ptr_unique.cpp | 4 +- 49 files changed, 80 insertions(+), 175 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 21ef8315..7fd982b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,7 @@ cmake --build . - `BOOST_OPENMETHOD_BUILD_TESTS` - Enable tests (default: ON if root project) - `BOOST_OPENMETHOD_BUILD_EXAMPLES` - Enable examples (requires tests enabled) - `BOOST_OPENMETHOD_WARNINGS_AS_ERRORS` - Treat warnings as errors +- `BOOST_OPENMETHOD_ENABLE_REFLECTION` - Build the tests and examples with C++26 reflection; probes the compiler for the flags it needs (`-std=c++26 -freflection` on GCC 16) and fails the configure if it has none - `BOOST_SRC_DIR` - Path to Boost source directory (default: `../..` or `$BOOST_SRC_DIR` env var) ### Boost.Build (b2) diff --git a/CMakeLists.txt b/CMakeLists.txt index d9d28038..9009fd53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,43 +75,29 @@ option( set(BOOST_OPENMETHOD_REFLECTION_OPTIONS "") if (BOOST_OPENMETHOD_ENABLE_REFLECTION) - include(CheckCXXSourceCompiles) - # The probe passes its own -std flag, and try_compile appends the one it - # derives from CMAKE_CXX_STANDARD *after* CMAKE_REQUIRED_FLAGS (policy - # CMP0067), where it would win: a configure that sets the standard - a - # preset, a toolchain file, a consuming project - would fail both candidates - # and stop at the FATAL_ERROR below. Take the variables out of the probe's - # way, and put them back after. Set to nothing rather than unset: a normal - # variable hides a cache one - a -D on the command line - where unset would - # expose it, and try_compile leaves an empty standard alone. - set(BOOST_OPENMETHOD_SAVED_CXX_STANDARD "${CMAKE_CXX_STANDARD}") - set(BOOST_OPENMETHOD_SAVED_CXX_EXTENSIONS "${CMAKE_CXX_EXTENSIONS}") - set(CMAKE_CXX_STANDARD "") - set(CMAKE_CXX_EXTENSIONS "") - - set(BOOST_OPENMETHOD_REFLECTION_TEST_SOURCE [[ - #include - struct Base {}; - struct Derived : Base {}; - consteval auto count() -> int { - return static_cast( - std::meta::bases_of( - ^^Derived, std::meta::access_context::unchecked()).size()); - } - static_assert(count() == 1); - int main() {} - ]]) - - set(CMAKE_REQUIRED_QUIET ON) + # derives from CMAKE_CXX_STANDARD *after* the definitions (policy CMP0067), + # where it would win: a configure that sets the standard - a preset, a + # toolchain file, a consuming project - would fail both candidates and stop + # at the FATAL_ERROR below. Take the variables out of the probe's way, and + # put them back after. Set to nothing rather than unset: a normal variable + # hides a cache one - a -D on the command line - where unset would expose + # it, and try_compile leaves an empty standard alone. The probe source is + # the one b2 uses (config/Jamfile); like b2, compile it only - it has no + # main - which the target type arranges. + foreach(var CMAKE_CXX_STANDARD CMAKE_CXX_EXTENSIONS CMAKE_TRY_COMPILE_TARGET_TYPE) + set(BOOST_OPENMETHOD_SAVED_${var} "${${var}}") + set(${var} "") + endforeach() + set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) foreach(candidate "-std=c++26" "-std=c++26;-freflection") - string(REPLACE ";" " " candidate_flags "${candidate}") - set(CMAKE_REQUIRED_FLAGS "${candidate_flags}") unset(BOOST_OPENMETHOD_HAS_REFLECTION CACHE) - check_cxx_source_compiles( - "${BOOST_OPENMETHOD_REFLECTION_TEST_SOURCE}" - BOOST_OPENMETHOD_HAS_REFLECTION) + try_compile( + BOOST_OPENMETHOD_HAS_REFLECTION + "${CMAKE_BINARY_DIR}/CMakeFiles/CMakeTmp" + SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/config/has_reflection.cpp" + COMPILE_DEFINITIONS ${candidate}) if (BOOST_OPENMETHOD_HAS_REFLECTION) set(BOOST_OPENMETHOD_REFLECTION_OPTIONS ${candidate}) @@ -119,12 +105,9 @@ if (BOOST_OPENMETHOD_ENABLE_REFLECTION) endif() endforeach() - unset(CMAKE_REQUIRED_FLAGS) - unset(CMAKE_REQUIRED_QUIET) - # An empty one is unset, not set to nothing, which a target would take for # an invalid standard. - foreach(var CMAKE_CXX_STANDARD CMAKE_CXX_EXTENSIONS) + foreach(var CMAKE_CXX_STANDARD CMAKE_CXX_EXTENSIONS CMAKE_TRY_COMPILE_TARGET_TYPE) if ("${BOOST_OPENMETHOD_SAVED_${var}}" STREQUAL "") unset(${var}) else() diff --git a/include/boost/openmethod/detail/reflection.hpp b/include/boost/openmethod/detail/reflection.hpp index 0a74fe92..c0e081df 100644 --- a/include/boost/openmethod/detail/reflection.hpp +++ b/include/boost/openmethod/detail/reflection.hpp @@ -41,14 +41,13 @@ namespace boost::openmethod::detail { template struct reflection_group { std::meta::info items[Size ? Size : 1]{}; - std::size_t size = Size; + static constexpr std::size_t size = Size; consteval reflection_group() = default; template requires(... && std::is_same_v) - consteval reflection_group(T... items_) : - items{items_...}, size(sizeof...(T)) { + consteval reflection_group(T... items_) : items{items_...} { } }; diff --git a/test/dynamic_loading/main.cpp b/test/dynamic_loading/main.cpp index 8ff79d59..0f863cd5 100644 --- a/test/dynamic_loading/main.cpp +++ b/test/dynamic_loading/main.cpp @@ -150,6 +150,4 @@ BOOST_AUTO_TEST_CASE(test_shared_state) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/dynamic_loading/method.cpp b/test/dynamic_loading/method.cpp index a4480989..fcefca45 100644 --- a/test/dynamic_loading/method.cpp +++ b/test/dynamic_loading/method.cpp @@ -62,6 +62,4 @@ BOOST_SYMBOL_EXPORT void method_call_meet( } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/dynamic_loading/overrider.cpp b/test/dynamic_loading/overrider.cpp index e2af95b7..23788cf3 100644 --- a/test/dynamic_loading/overrider.cpp +++ b/test/dynamic_loading/overrider.cpp @@ -56,6 +56,4 @@ BOOST_SYMBOL_EXPORT void overrider_call_meet( } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/dynamic_loading/registry.cpp b/test/dynamic_loading/registry.cpp index 174848f8..0bc70dfe 100644 --- a/test/dynamic_loading/registry.cpp +++ b/test/dynamic_loading/registry.cpp @@ -23,6 +23,4 @@ namespace mp11 = boost::mp11; BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/custom_registry/lib.cpp b/test/implicit_shared_libraries/custom_registry/lib.cpp index 813cec89..fac7983b 100644 --- a/test/implicit_shared_libraries/custom_registry/lib.cpp +++ b/test/implicit_shared_libraries/custom_registry/lib.cpp @@ -47,6 +47,4 @@ auto lib_make_dog() -> unique_virtual_ptr { return make_unique_virtual(); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/custom_registry/lib2.cpp b/test/implicit_shared_libraries/custom_registry/lib2.cpp index a68b726c..dc22c743 100644 --- a/test/implicit_shared_libraries/custom_registry/lib2.cpp +++ b/test/implicit_shared_libraries/custom_registry/lib2.cpp @@ -31,6 +31,4 @@ auto lib_make_cow() -> unique_virtual_ptr { return make_unique_virtual(); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/custom_registry/main.cpp b/test/implicit_shared_libraries/custom_registry/main.cpp index b8e3b09c..b1806b4b 100644 --- a/test/implicit_shared_libraries/custom_registry/main.cpp +++ b/test/implicit_shared_libraries/custom_registry/main.cpp @@ -63,6 +63,4 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { BOOST_TEST(std::string(lib_speak(animal)) == "?"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/default_registry/lib.cpp b/test/implicit_shared_libraries/default_registry/lib.cpp index 02f3ceb4..dc331510 100644 --- a/test/implicit_shared_libraries/default_registry/lib.cpp +++ b/test/implicit_shared_libraries/default_registry/lib.cpp @@ -47,6 +47,4 @@ auto lib_make_dog() -> unique_virtual_ptr { return make_unique_virtual(); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/default_registry/main.cpp b/test/implicit_shared_libraries/default_registry/main.cpp index 35c6b2ce..c7a8d2e8 100644 --- a/test/implicit_shared_libraries/default_registry/main.cpp +++ b/test/implicit_shared_libraries/default_registry/main.cpp @@ -82,6 +82,4 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { BOOST_TEST(std::string(lib_speak(animal)) == "?"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_classes.hpp b/test/test_classes.hpp index 7b2b9081..0bc14c99 100644 --- a/test/test_classes.hpp +++ b/test/test_classes.hpp @@ -22,6 +22,16 @@ //! unless that macro - or `register_classes` - is used, so the class they //! withhold stays withheld under either standard. +//! Register the classes of the translation unit by reflection, when the +//! compiler supports it - a no-op otherwise. +//! +//! The counterpart of @ref BOOST_OPENMETHOD_TEST_CLASSES: a test lists its +//! classes with that one, for C++17, and ends with this one, for C++26. Last in +//! the file, because the standard wants the registrar after the declarations it +//! selects. +#define BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(...) \ + BOOST_OPENMETHOD_REGISTER_CLASSES(__VA_ARGS__) + #if BOOST_OPENMETHOD_HAS_REFLECTION #define BOOST_OPENMETHOD_TEST_CLASSES(...) #else diff --git a/test/test_custom_rtti_deferred.cpp b/test/test_custom_rtti_deferred.cpp index 3f165ddd..6ba8a4fd 100644 --- a/test/test_custom_rtti_deferred.cpp +++ b/test/test_custom_rtti_deferred.cpp @@ -236,6 +236,4 @@ BOOST_AUTO_TEST_CASE(custom_rtti_deferred) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_custom_rtti_simple.cpp b/test/test_custom_rtti_simple.cpp index e0f23022..75c8f4d0 100644 --- a/test/test_custom_rtti_simple.cpp +++ b/test/test_custom_rtti_simple.cpp @@ -169,6 +169,4 @@ void call_poke(vptr a, std::ostream& os) { } // namespace using_vptr -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_custom_rtti_simple_projection.cpp b/test/test_custom_rtti_simple_projection.cpp index 53c8d73f..e8fcd358 100644 --- a/test/test_custom_rtti_simple_projection.cpp +++ b/test/test_custom_rtti_simple_projection.cpp @@ -117,6 +117,4 @@ BOOST_AUTO_TEST_CASE(custom_rtti_simple_projection) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_custom_rtti_virtual_base.cpp b/test/test_custom_rtti_virtual_base.cpp index bf836951..dd337048 100644 --- a/test/test_custom_rtti_virtual_base.cpp +++ b/test/test_custom_rtti_virtual_base.cpp @@ -194,6 +194,4 @@ void call_poke(vptr a, std::ostream& os) { } // namespace using_vptr -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_across_namespaces.cpp b/test/test_dispatch_across_namespaces.cpp index 852abeeb..f00c2f4a 100644 --- a/test/test_dispatch_across_namespaces.cpp +++ b/test/test_dispatch_across_namespaces.cpp @@ -46,6 +46,4 @@ BOOST_AUTO_TEST_CASE(across_namespaces) { BOOST_TEST("bark" == poke(animal)); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp index f8587e92..d2cbfcda 100644 --- a/test/test_dispatch_boost_any.cpp +++ b/test/test_dispatch_boost_any.cpp @@ -402,6 +402,4 @@ BOOST_AUTO_TEST_CASE(boost_any_class_in_hierarchy) { } } // namespace BOOST_OPENMETHOD_GENSYM -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_comma_in_return_type.cpp b/test/test_dispatch_comma_in_return_type.cpp index 028f5e70..1aef3045 100644 --- a/test/test_dispatch_comma_in_return_type.cpp +++ b/test/test_dispatch_comma_in_return_type.cpp @@ -35,6 +35,4 @@ BOOST_AUTO_TEST_CASE(comma_in_return_type) { BOOST_CHECK(foo(test) == std::pair(1, 2)); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_intrusive_ptr.cpp b/test/test_dispatch_intrusive_ptr.cpp index c0b4dbeb..58cdbdcc 100644 --- a/test/test_dispatch_intrusive_ptr.cpp +++ b/test/test_dispatch_intrusive_ptr.cpp @@ -169,6 +169,4 @@ BOOST_AUTO_TEST_CASE(intrusive_virtual_ptr_by_const_ref) { } } // namespace BOOST_OPENMETHOD_GENSYM -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_lvalue_refs.cpp b/test/test_dispatch_lvalue_refs.cpp index 2beca7f7..86128fe9 100644 --- a/test/test_dispatch_lvalue_refs.cpp +++ b/test/test_dispatch_lvalue_refs.cpp @@ -38,6 +38,4 @@ BOOST_AUTO_TEST_CASE(cast_args_lvalue_refs) { BOOST_TEST(name(felix) == "Bill's cat Felix"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_multi.cpp b/test/test_dispatch_multi.cpp index da2be109..43f49999 100644 --- a/test/test_dispatch_multi.cpp +++ b/test/test_dispatch_multi.cpp @@ -73,6 +73,4 @@ BOOST_AUTO_TEST_CASE(simple) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_next_fn.cpp b/test/test_dispatch_next_fn.cpp index e3fbaeff..d0808328 100644 --- a/test/test_dispatch_next_fn.cpp +++ b/test/test_dispatch_next_fn.cpp @@ -52,6 +52,4 @@ BOOST_AUTO_TEST_CASE(test_next_fn) { BOOST_TEST(poke::fn(*hector) == "bark and bite back"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_pointer.cpp b/test/test_dispatch_pointer.cpp index 48821931..a2dc151c 100644 --- a/test/test_dispatch_pointer.cpp +++ b/test/test_dispatch_pointer.cpp @@ -38,6 +38,4 @@ BOOST_AUTO_TEST_CASE(cast_args_pointer) { BOOST_TEST(name(&felix) == "Bill's cat Felix"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_rvalue_refs.cpp b/test/test_dispatch_rvalue_refs.cpp index 20ab935c..7ce3cefd 100644 --- a/test/test_dispatch_rvalue_refs.cpp +++ b/test/test_dispatch_rvalue_refs.cpp @@ -46,6 +46,4 @@ BOOST_AUTO_TEST_CASE(cast_args_rvalue_refs) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_shared_ptr_by_ref.cpp b/test/test_dispatch_shared_ptr_by_ref.cpp index 36ecf122..c6e31b1b 100644 --- a/test/test_dispatch_shared_ptr_by_ref.cpp +++ b/test/test_dispatch_shared_ptr_by_ref.cpp @@ -43,6 +43,4 @@ BOOST_AUTO_TEST_CASE(cast_args_shared_ptr_by_ref) { BOOST_TEST(name(felix) == "Bill's cat Felix"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_shared_ptr_by_value.cpp b/test/test_dispatch_shared_ptr_by_value.cpp index 7fc8a35b..8282cf72 100644 --- a/test/test_dispatch_shared_ptr_by_value.cpp +++ b/test/test_dispatch_shared_ptr_by_value.cpp @@ -40,6 +40,4 @@ BOOST_AUTO_TEST_CASE(cast_args_shared_ptr_by_value) { BOOST_TEST(name(felix) == "Bill's cat Felix"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 5d2ca131..5500b483 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -399,6 +399,4 @@ BOOST_AUTO_TEST_CASE(std_any_class_in_hierarchy) { } } // namespace BOOST_OPENMETHOD_GENSYM -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_dispatch_unique_ptr.cpp b/test/test_dispatch_unique_ptr.cpp index f3141b16..004e653e 100644 --- a/test/test_dispatch_unique_ptr.cpp +++ b/test/test_dispatch_unique_ptr.cpp @@ -42,6 +42,4 @@ BOOST_AUTO_TEST_CASE(cast_args_unique_ptr) { BOOST_TEST(felix.get() == nullptr); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_n2216_covariant_return_type.cpp b/test/test_n2216_covariant_return_type.cpp index 5878b6b6..a1271d66 100644 --- a/test/test_n2216_covariant_return_type.cpp +++ b/test/test_n2216_covariant_return_type.cpp @@ -48,6 +48,4 @@ BOOST_AUTO_TEST_CASE(covariant_return_type) { BOOST_TEST(result->type == DENSE_MATRIX); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_n2216_pick_any_ambiguous.cpp b/test/test_n2216_pick_any_ambiguous.cpp index 8eaa2df2..c3b7d0c1 100644 --- a/test/test_n2216_pick_any_ambiguous.cpp +++ b/test/test_n2216_pick_any_ambiguous.cpp @@ -44,6 +44,4 @@ BOOST_AUTO_TEST_CASE(pick_any_ambiguous) { BOOST_TEST(result.second == MATRIX_MATRIX); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_namespaces.cpp b/test/test_namespaces.cpp index 024aa841..a7a6ad2a 100644 --- a/test/test_namespaces.cpp +++ b/test/test_namespaces.cpp @@ -108,6 +108,4 @@ auto main() -> int { << "\n"; // ignore } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_pointer_to_method.cpp b/test/test_pointer_to_method.cpp index 94fde84b..127c83d8 100644 --- a/test/test_pointer_to_method.cpp +++ b/test/test_pointer_to_method.cpp @@ -38,6 +38,4 @@ BOOST_AUTO_TEST_CASE(noadl) { BOOST_TEST(stimulus(snoopy) == "bark"); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_rolex.cpp b/test/test_rolex.cpp index 9a49f2cd..11ea9a7b 100644 --- a/test/test_rolex.cpp +++ b/test/test_rolex.cpp @@ -187,6 +187,4 @@ BOOST_AUTO_TEST_CASE(approve_via_wrapper) { BOOST_TEST(call_approve(f, taxi, 10) == true); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_bad_call.cpp b/test/test_runtime_errors_bad_call.cpp index 6ab2cce5..7274e0c3 100644 --- a/test/test_runtime_errors_bad_call.cpp +++ b/test/test_runtime_errors_bad_call.cpp @@ -54,6 +54,4 @@ BOOST_AUTO_TEST_CASE(bad_calls) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_bad_call_type_ids.cpp b/test/test_runtime_errors_bad_call_type_ids.cpp index fedcac7f..d22126bf 100644 --- a/test/test_runtime_errors_bad_call_type_ids.cpp +++ b/test/test_runtime_errors_bad_call_type_ids.cpp @@ -49,6 +49,4 @@ BOOST_AUTO_TEST_CASE(bad_call_type_ids) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp b/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp index ff83bb95..e89bdd52 100644 --- a/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp +++ b/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp @@ -42,6 +42,4 @@ BOOST_AUTO_TEST_CASE(bad_call_type_ids_smart_ptr) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_duplicate_overrider.cpp b/test/test_runtime_errors_duplicate_overrider.cpp index 98e87f8e..e2e064eb 100644 --- a/test/test_runtime_errors_duplicate_overrider.cpp +++ b/test/test_runtime_errors_duplicate_overrider.cpp @@ -61,6 +61,4 @@ BOOST_AUTO_TEST_CASE(duplicate_overrider_is_ambiguous) { BOOST_TEST(capture().find("ambiguous") != std::string::npos); } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_runtime_errors_throw_error.cpp b/test/test_runtime_errors_throw_error.cpp index 6e2ef5ab..352b697d 100644 --- a/test/test_runtime_errors_throw_error.cpp +++ b/test/test_runtime_errors_throw_error.cpp @@ -42,6 +42,4 @@ BOOST_AUTO_TEST_CASE(throw_error) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_smart_virtual_ptr_value_semantics.cpp b/test/test_smart_virtual_ptr_value_semantics.cpp index 6bf464ff..1105c7b5 100644 --- a/test/test_smart_virtual_ptr_value_semantics.cpp +++ b/test/test_smart_virtual_ptr_value_semantics.cpp @@ -6,6 +6,8 @@ #include #include +#include "test_classes.hpp" + #define BOOST_TEST_MODULE openmethod #include @@ -444,6 +446,4 @@ template struct check_illegal_smart_ops< template struct check_illegal_smart_ops< boost::intrusive_ptr, std::unique_ptr, direct_vector>; -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_static_rtti.cpp b/test/test_static_rtti.cpp index ccdf4706..1c658e84 100644 --- a/test/test_static_rtti.cpp +++ b/test/test_static_rtti.cpp @@ -57,6 +57,4 @@ BOOST_AUTO_TEST_CASE(static_rtti) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_virtual_any_boost.cpp b/test/test_virtual_any_boost.cpp index 0fad5bce..69c4e299 100644 --- a/test/test_virtual_any_boost.cpp +++ b/test/test_virtual_any_boost.cpp @@ -337,6 +337,4 @@ BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { } } // namespace BOOST_OPENMETHOD_GENSYM -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_virtual_any_std.cpp b/test/test_virtual_any_std.cpp index d2ce6a3b..2ae2be5e 100644 --- a/test/test_virtual_any_std.cpp +++ b/test/test_virtual_any_std.cpp @@ -337,6 +337,4 @@ BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { } } // namespace BOOST_OPENMETHOD_GENSYM -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_by_ref.cpp b/test/test_virtual_ptr_by_ref.cpp index a6f64646..849f056f 100644 --- a/test/test_virtual_ptr_by_ref.cpp +++ b/test/test_virtual_ptr_by_ref.cpp @@ -63,6 +63,4 @@ BOOST_AUTO_TEST_CASE(test_virtual_ptr_by_ref) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_non_polymorphic.cpp b/test/test_virtual_ptr_non_polymorphic.cpp index 5aa93dd4..c13ad9f7 100644 --- a/test/test_virtual_ptr_non_polymorphic.cpp +++ b/test/test_virtual_ptr_non_polymorphic.cpp @@ -40,6 +40,4 @@ BOOST_AUTO_TEST_CASE(test_virtual_ptr_non_polymorphic) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_shared_by_const_ref.cpp b/test/test_virtual_ptr_shared_by_const_ref.cpp index 3801aca4..44e36318 100644 --- a/test/test_virtual_ptr_shared_by_const_ref.cpp +++ b/test/test_virtual_ptr_shared_by_const_ref.cpp @@ -45,6 +45,4 @@ BOOST_AUTO_TEST_CASE(test_virtual_shared_by_const_reference) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_shared_by_value.cpp b/test/test_virtual_ptr_shared_by_value.cpp index fa455061..123b8a96 100644 --- a/test/test_virtual_ptr_shared_by_value.cpp +++ b/test/test_virtual_ptr_shared_by_value.cpp @@ -43,6 +43,4 @@ BOOST_AUTO_TEST_CASE(test_virtual_shared_by_value) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_virtual_ptr_unique.cpp b/test/test_virtual_ptr_unique.cpp index dcbf8482..8d7c1007 100644 --- a/test/test_virtual_ptr_unique.cpp +++ b/test/test_virtual_ptr_unique.cpp @@ -43,6 +43,4 @@ BOOST_AUTO_TEST_CASE(test_virtual_unique) { } } -// Registers the classes above by reflection, when the compiler supports it. -// Last in the file: the standard wants the registrar after what it selects. -BOOST_OPENMETHOD_REGISTER_CLASSES(); +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES();