diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 874b7827..370e3b82 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,6 +54,41 @@ jobs:
COVERITY_SCAN_NOTIFICATION_EMAIL: ${{ secrets.COVERITY_SCAN_NOTIFICATION_EMAIL }}
COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
+ reflection:
+ name: C++26 reflection
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Install GCC 16
+ run: |
+ sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
+ sudo apt-get update
+ sudo apt-get install -y g++-16 cmake ninja-build
+
+ - name: Clone Boost.OpenMethod
+ uses: actions/checkout@v4
+
+ - name: Clone Boost
+ uses: alandefreitas/cpp-actions/boost-clone@v1.8.8
+ with:
+ branch: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }}
+ boost-dir: ../boost-source
+ scan-modules-dir: .
+ scan-modules-ignore: openmethod
+
+ # The suite is the reflection test: BOOST_OPENMETHOD_TEST_CLASSES expands
+ # to nothing here, so every class has to be found by use_classes_in.
+ - name: Build and test
+ run: |
+ cmake -S . -B ../build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DCMAKE_CXX_COMPILER=g++-16 \
+ -DBOOST_OPENMETHOD_ENABLE_REFLECTION=ON \
+ -DBOOST_OPENMETHOD_BUILD_TESTS=ON \
+ -DBOOST_OPENMETHOD_WARNINGS_AS_ERRORS=ON \
+ -DBOOST_SRC_DIR="$(cd .. && pwd)/boost-source"
+ cmake --build ../build --target tests -j $(nproc)
+ ctest --test-dir ../build -j $(nproc) --output-on-failure
+
antora:
name: Antora docs
strategy:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5224a251..9f119dfd 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -59,6 +59,81 @@ option(
BOOST_OPENMETHOD_WARNINGS_AS_ERRORS
"Treat warnings as errors"
OFF)
+option(
+ BOOST_OPENMETHOD_ENABLE_REFLECTION
+ "Build the tests and examples with C++26 reflection enabled"
+ 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.
+set(BOOST_OPENMETHOD_REFLECTION_OPTIONS "")
+
+if (BOOST_OPENMETHOD_ENABLE_REFLECTION)
+ include(CheckCXXSourceCompiles)
+
+ 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)
+
+ 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)
+
+ if (BOOST_OPENMETHOD_HAS_REFLECTION)
+ set(BOOST_OPENMETHOD_REFLECTION_OPTIONS ${candidate})
+ break()
+ endif()
+ endforeach()
+
+ unset(CMAKE_REQUIRED_FLAGS)
+ unset(CMAKE_REQUIRED_QUIET)
+
+ if (NOT BOOST_OPENMETHOD_HAS_REFLECTION)
+ message(
+ FATAL_ERROR
+ "BOOST_OPENMETHOD_ENABLE_REFLECTION is ON but ${CMAKE_CXX_COMPILER_ID} "
+ "${CMAKE_CXX_COMPILER_VERSION} does not support C++26 reflection")
+ endif()
+
+ message(
+ 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()
if (BOOST_OPENMETHOD_BUILD_EXAMPLES AND NOT BOOST_OPENMETHOD_BUILD_TESTS)
message(
diff --git a/doc/modules/ROOT/examples/CMakeLists.txt b/doc/modules/ROOT/examples/CMakeLists.txt
index f922ab9e..2b0555f2 100644
--- a/doc/modules/ROOT/examples/CMakeLists.txt
+++ b/doc/modules/ROOT/examples/CMakeLists.txt
@@ -21,6 +21,7 @@ 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})
@@ -43,6 +44,7 @@ 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 0d50f66d..f0128fe7 100644
--- a/doc/modules/ROOT/pages/basics.adoc
+++ b/doc/modules/ROOT/pages/basics.adoc
@@ -83,6 +83,13 @@ direct base of a class must appear together with it in at least one call to
`BOOST_OPENMETHOD_CLASSES`. This enables the library to deduce the complete
inheritance lattice.
+[NOTE]
+====
+With a compiler that supports C++26 reflection, the class list is unnecessary.
+xref:reference:BOOST_OPENMETHOD_CLASSES_IN.adoc[BOOST_OPENMETHOD_CLASSES_IN]
+finds the classes on its own - see <>.
+====
+
The constructs used in this example require the classes to be polymorphic, in
the standard C++ sense, i.e. they must have at least one virtual function. The
library can also be used with non-polymorphic classes, with some restrictions.
@@ -107,3 +114,88 @@ Putting it all together:
----
include::{examplesdir}/ast.cpp[tag=content]
----
+
+
+[#registering_classes_by_reflection]
+## Registering Classes by Reflection
+
+When the compiler supports C++26 reflection (P2996), the library can work out
+the class list for itself. One call to
+xref:reference:BOOST_OPENMETHOD_CLASSES_IN.adoc[BOOST_OPENMETHOD_CLASSES_IN]
+replaces every
+xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES] in the
+file:
+
+[source,c++]
+----
+struct Animal { virtual ~Animal() = default; };
+struct Cat : Animal {};
+struct Dog : Animal {};
+struct Bulldog : Dog {};
+
+BOOST_OPENMETHOD(poke, (std::ostream&, virtual_), void);
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (std::ostream& os, Dog&), void) {
+ os << "bark";
+}
+
+BOOST_OPENMETHOD_CLASSES_IN(::); // registers all four classes
+----
+
+The macro scans the namespace it is given - and the namespaces nested in it -
+for the methods of the registry. It collects the classes those methods dispatch
+on, then registers them, along with every class in the scanned namespaces that
+derives from one of them. `Bulldog` above has no overrider of its own and is
+named nowhere, and is registered all the same.
+
+A base class that no method dispatches on is *not* registered - it could never
+be selected on, and registering it would cost a lattice node, a hash slot and
+dispatch table space for nothing. So a hierarchy 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.
+
+Classes declared in the standard library's or the compiler's own headers are not
+scanned, so `::` costs little more than the narrower namespace would.
+
+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.
+
+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.
+
+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
+`BOOST_OPENMETHOD_CLASSES` therefore builds under either standard.
+
+### What Reflection Cannot Find
+
+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;
+* 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 9d67461a..84e55776 100644
--- a/doc/modules/ROOT/pages/core_api.adoc
+++ b/doc/modules/ROOT/pages/core_api.adoc
@@ -78,6 +78,22 @@ We register the classes with `use_classes`:
include::{example}/core_api.cpp[tag=use_classes]
----
+With a compiler that supports C++26 reflection, `use_classes_in` replaces that
+list. It scans a namespace for the registry's methods, and registers the classes
+they dispatch on along with everything in the namespace that derives from them:
+
+[source,c++]
+----
+BOOST_OPENMETHOD_REGISTER(use_classes_in<^^::>);
+----
+
+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
+xref:ROOT:basics.adoc#registering_classes_by_reflection[Registering Classes by
+Reflection].
+
Finally, we call the method via the static member of the method class `fn`:
[source,c++]
diff --git a/doc/modules/ROOT/pages/ref_macros.adoc b/doc/modules/ROOT/pages/ref_macros.adoc
index c3250ee9..502957ca 100644
--- a/doc/modules/ROOT/pages/ref_macros.adoc
+++ b/doc/modules/ROOT/pages/ref_macros.adoc
@@ -14,6 +14,8 @@ uses of the library.
| Adds an overrider to a method.
| xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[*BOOST_OPENMETHOD_CLASSES*]
| Registers classes.
+| xref:reference:BOOST_OPENMETHOD_CLASSES_IN.adoc[*BOOST_OPENMETHOD_CLASSES_IN*]
+| Registers the classes of a namespace, by reflection.
| xref:reference:BOOST_OPENMETHOD_INLINE_OVERRIDE.adoc[BOOST_OPENMETHOD_INLINE_OVERRIDE]
| Adds an overrider to a method as an inline function.
| xref:reference:BOOST_OPENMETHOD_DECLARE_OVERRIDER.adoc[BOOST_OPENMETHOD_DECLARE_OVERRIDER]
diff --git a/doc/modules/ROOT/pages/registries_and_policies.adoc b/doc/modules/ROOT/pages/registries_and_policies.adoc
index e7e0002e..bfd16427 100644
--- a/doc/modules/ROOT/pages/registries_and_policies.adoc
+++ b/doc/modules/ROOT/pages/registries_and_policies.adoc
@@ -92,6 +92,15 @@ 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
+xref:reference:use_classes.adoc[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 1b7b8c3e..de47749b 100644
--- a/doc/modules/ROOT/snippets/CMakeLists.txt
+++ b/doc/modules/ROOT/snippets/CMakeLists.txt
@@ -23,6 +23,7 @@ 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 fc557b5e..20288f4f 100644
--- a/doc/modules/ROOT/snippets/errors_missing_base.cpp
+++ b/doc/modules/ROOT/snippets/errors_missing_base.cpp
@@ -3,6 +3,8 @@
// 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 5f6a2bc1..9b5d42f3 100644
--- a/doc/modules/ROOT/snippets/errors_missing_class_call.cpp
+++ b/doc/modules/ROOT/snippets/errors_missing_class_call.cpp
@@ -7,6 +7,8 @@
// 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 4c4095c5..e47d28ba 100644
--- a/doc/modules/ROOT/snippets/errors_missing_class_method.cpp
+++ b/doc/modules/ROOT/snippets/errors_missing_class_method.cpp
@@ -3,6 +3,8 @@
// 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 31ad6bbb..65652dfa 100644
--- a/doc/modules/ROOT/snippets/errors_missing_class_overrider.cpp
+++ b/doc/modules/ROOT/snippets/errors_missing_class_overrider.cpp
@@ -3,6 +3,8 @@
// 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
new file mode 100644
index 00000000..b70afbc0
--- /dev/null
+++ b/doc/modules/ROOT/snippets/explicit_registration.hpp
@@ -0,0 +1,27 @@
+// 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
+
+#include
+
+struct snippet_registry
+ : boost::openmethod::default_registry::with<
+ boost::openmethod::policies::explicit_class_registration> {};
+
+#define BOOST_OPENMETHOD_DEFAULT_REGISTRY snippet_registry
+
+#endif
diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp
index 0ffdafc1..89173622 100644
--- a/include/boost/openmethod/core.hpp
+++ b/include/boost/openmethod/core.hpp
@@ -22,6 +22,7 @@
#include
#include
+#include
#ifndef BOOST_OPENMETHOD_DEFAULT_REGISTRY
//! Default value for `Registry`.
@@ -500,6 +501,33 @@ class use_classes {
detail::use_classes_tuple_type tuple;
};
+// -----------------------------------------------------------------------------
+// reflection-based class registration
+
+namespace detail {
+
+#if BOOST_OPENMETHOD_HAS_REFLECTION
+
+// One registrar per entry, for the whole program - not per translation unit, as
+// `BOOST_OPENMETHOD_CLASSES` produces. Same mechanism as
+// `inplace_vptr_use_classes`: an `inline` variable template, instantiated by
+// odr-use. Keyed on the entry rather than on the class, because a class' base
+// list depends on what else was registered alongside it.
+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.
+template
+BOOST_FORCEINLINE auto
+use_reflected_classes(mp11::mp_list*) -> void {
+ (..., (void)&reflected_class_registrar);
+}
+
+#endif
+
+} // namespace detail
+
// =============================================================================
// virtual_ptr
@@ -2758,6 +2786,227 @@ void method::override_impl<
this->vp_type_ids);
}
+// =============================================================================
+// use_classes_in
+
+namespace detail {
+
+#if BOOST_OPENMETHOD_HAS_REFLECTION
+
+template
+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>;
+};
+
+// Read from reflection, by `substitute`-ing a method into it and taking the
+// template arguments of the result.
+template
+using method_classes = typename method_traits_aux::type;
+
+// The classes to register for a scan of `Namespace`, each with its direct
+// bases: the classes the methods of `Registry` dispatch on, and the ones the
+// scan 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 a system header, 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 {
+ std::vector methods, classes;
+ scan_namespace(Namespace, ^^method, methods, classes);
+
+ // The classes the methods dispatch on.
+ std::vector virtual_classes;
+
+ for (auto found : methods) {
+ // A method's third template argument is its registry.
+ if (std::meta::template_arguments_of(found)[2] != ^^Registry) {
+ 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));
+ }
+ }
+ }
+
+ // 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;
+
+ for (auto found : classes) {
+ std::vector bases;
+ collect_reflected_bases(found, bases);
+
+ for (auto base : bases) {
+ if (contains(virtual_classes, base)) {
+ push_unique(registered, found);
+ break;
+ }
+ }
+ }
+
+ // 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));
+
+ for (auto index = 0u; index != count; ++index) {
+ std::vector bases;
+ collect_reflected_bases(registered[index], bases);
+
+ for (auto base : bases) {
+ if (base == registered[index]) {
+ continue;
+ }
+
+ for (auto other = 0u; other != count; ++other) {
+ if (registered[other] == base) {
+ inherits[index * count + other] = char(1);
+ break;
+ }
+ }
+ }
+ }
+
+ std::vector entries;
+
+ 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;
+ }
+
+ // 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;
+ }
+ }
+
+ if (!hidden) {
+ entry.push_back(registered[base]);
+ }
+ }
+
+ entries.push_back(std::meta::substitute(^^mp11::mp_list, entry));
+ }
+
+ return std::meta::substitute(^^mp11::mp_list, entries);
+}
+
+// `mp_list, ...>`, ready for `use_class_aux`.
+//
+// clang-format off: the formatter predates P2996 and eats the spaces around the
+// splice, leaving `typename[:...:]`.
+template
+using reflected_registered_classes =
+ typename [: reflected_registered_classes_info() :];
+// clang-format on
+
+#endif
+
+} // namespace detail
+
+//! Add the classes of a namespace to a registry
+//!
+//! `use_classes_in` is a registrar class that finds the classes taking part in
+//! dispatch by reflection, and adds them to a registry. It makes @ref
+//! use_classes unnecessary in most cases.
+//!
+//! It scans `Namespace`, and the namespaces nested in it, for the methods of
+//! `Registry`; collects the classes they dispatch on; and registers those,
+//! along with every class in the scanned namespaces that derives from one of
+//! them. A base class that no method dispatches on is not registered: it could
+//! never be selected on. Classes declared in the standard library's or the
+//! compiler's own headers are not scanned.
+//!
+//! Reflection sees only what precedes it, so `use_classes_in` must come **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`
+//! declaration written by hand, or any of its registrar objects - the object
+//! @ref BOOST_OPENMETHOD_OVERRIDE creates, or one written by hand. None of
+//! 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.
+//!
+//! Virtual and multiple inheritance are supported. Unlike @ref use_classes,
+//! which rejects it, repeated inheritance is not an error here: an ambiguous
+//! base cannot take part in dispatch, so it is left out.
+//!
+//! This class template is available only if the compiler supports C++26
+//! reflection, i.e. if `BOOST_OPENMETHOD_HAS_REFLECTION` is 1.
+//!
+//! @tparam Namespace A reflection of a namespace, e.g. `^^::`.
+//! @tparam Registry A @ref registry.
+//!
+//! @see [Core API](xref:ROOT:core_api.adoc)
+#if BOOST_OPENMETHOD_HAS_REFLECTION
+template<
+ std::meta::info Namespace,
+ class Registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY>
+class use_classes_in {
+ public:
+ use_classes_in() {
+ if constexpr (Registry::has_reflected_class_registration) {
+ using registered =
+ detail::reflected_registered_classes;
+ detail::use_reflected_classes(
+ static_cast(nullptr));
+ }
+ }
+};
+#endif
+
//! Aliases for the most frequently used types in the library.
namespace aliases {
diff --git a/include/boost/openmethod/detail/reflection.hpp b/include/boost/openmethod/detail/reflection.hpp
new file mode 100644
index 00000000..67a5089f
--- /dev/null
+++ b/include/boost/openmethod/detail/reflection.hpp
@@ -0,0 +1,187 @@
+// 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)
+
+#ifndef BOOST_OPENMETHOD_DETAIL_REFLECTION_HPP
+#define BOOST_OPENMETHOD_DETAIL_REFLECTION_HPP
+
+// Detect C++26 reflection (P2996). Both the language feature (the `^^`
+// operator) and the library (`std::meta`) are required.
+#if !defined(BOOST_OPENMETHOD_HAS_REFLECTION)
+#if defined(__cpp_impl_reflection) && __has_include()
+#include
+#if defined(__cpp_lib_reflection)
+#define BOOST_OPENMETHOD_HAS_REFLECTION 1
+#endif
+#endif
+#endif
+
+#if !defined(BOOST_OPENMETHOD_HAS_REFLECTION)
+#define BOOST_OPENMETHOD_HAS_REFLECTION 0
+#endif
+
+#if BOOST_OPENMETHOD_HAS_REFLECTION
+
+#include
+#include
+
+#include
+
+namespace boost::openmethod::detail {
+
+// =============================================================================
+// 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.
+consteval void collect_reflected_bases(
+ std::meta::info type, std::vector& types) {
+ for (auto seen : types) {
+ if (seen == type) {
+ return;
+ }
+ }
+
+ types.push_back(type);
+
+ for (auto base :
+ std::meta::bases_of(type, std::meta::access_context::unchecked())) {
+ if (std::meta::is_public(base)) {
+ collect_reflected_bases(std::meta::type_of(base), types);
+ }
+ }
+}
+
+template
+consteval auto reflected_bases_info() -> std::meta::info {
+ std::vector types;
+ collect_reflected_bases(std::meta::dealias(^^Class), types);
+
+ return std::meta::substitute(^^mp11::mp_list, types);
+}
+
+// `mp11::mp_list`, where `Bases` are all the base classes
+// transitively reachable from `Class` through public inheritance, in
+// unspecified order. `Class` itself is the first element.
+template
+// clang-format off: the formatter predates P2996 and eats the spaces around
+// the splice, leaving `typename[:...:]`.
+using reflected_bases = typename [: reflected_bases_info() :];
+// clang-format on
+
+// =============================================================================
+// namespace scan
+
+// True if `member` comes from a header that is not part of the program: the
+// standard library, and the compiler's own headers. Scanning them would cost a
+// great deal and find nothing - a method cannot be declared on a class the
+// program has never heard of. Applied to a namespace as well, which is what
+// keeps `std` and its friends out of a scan of the global namespace.
+consteval auto in_system_header(std::meta::info member) -> bool {
+ auto file =
+ std::string_view(std::meta::source_location_of(member).file_name());
+
+ return file.starts_with("/usr/include") || file.starts_with("/usr/lib") ||
+ file.starts_with("/usr/local/include") ||
+ file.find("/include/c++/") != std::string_view::npos;
+}
+
+consteval auto contains(
+ const std::vector& types, std::meta::info type) -> bool {
+ for (auto seen : types) {
+ if (seen == type) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+consteval void
+push_unique(std::vector& types, std::meta::info type) {
+ if (!contains(types, type)) {
+ types.push_back(type);
+ }
+}
+
+// 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);
+
+ if (std::meta::is_type(parent) &&
+ std::meta::has_template_arguments(parent)) {
+ return parent;
+ }
+ }
+ }
+
+ return std::meta::info();
+}
+
+// Walk `ns` and the namespaces 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.
+consteval void scan_namespace(
+ std::meta::info ns, std::meta::info Template,
+ std::vector& specializations,
+ std::vector& classes) {
+ for (auto member :
+ std::meta::members_of(ns, std::meta::access_context::unchecked())) {
+ if (in_system_header(member)) {
+ continue;
+ }
+
+ if (std::meta::is_namespace(member)) {
+ scan_namespace(member, Template, specializations, classes);
+ continue;
+ }
+
+ auto specialization = specialization_named_by(member);
+
+ if (specialization != std::meta::info() &&
+ std::meta::template_of(specialization) == Template) {
+ push_unique(specializations, specialization);
+ }
+
+ if (std::meta::is_type(member)) {
+ auto type = std::meta::dealias(member);
+
+ if (std::meta::is_class_type(type) &&
+ std::meta::is_complete_type(type)) {
+ push_unique(classes, type);
+ }
+ }
+ }
+}
+
+} // namespace boost::openmethod::detail
+
+#endif
+
+#endif
diff --git a/include/boost/openmethod/initialize.hpp b/include/boost/openmethod/initialize.hpp
index 13756852..79942179 100644
--- a/include/boost/openmethod/initialize.hpp
+++ b/include/boost/openmethod/initialize.hpp
@@ -158,6 +158,9 @@ struct generic_compiler {
struct class_ {
std::vector ci;
+ // What the records declared, before the closure is computed; emptied by
+ // calculate_transitive_bases.
+ std::vector declared_bases;
std::vector transitive_bases;
std::vector direct_bases;
std::vector direct_derived;
@@ -167,6 +170,7 @@ struct generic_compiler {
boost::dynamic_bitset<> reserved_slots;
std::size_t first_slot = 0;
std::size_t mark = 0; // temporary mark to detect cycles
+ bool transitive_bases_done = false;
std::vector vtbl;
auto is_base_of(class_* other) const -> bool {
@@ -551,7 +555,7 @@ struct registry::compiler : detail::generic_compiler {
void install_global_tables();
void augment_classes();
- void collect_transitive_bases(class_* cls, class_* base);
+ void calculate_transitive_bases(class_& cls);
void calculate_transitive_derived(class_& cls);
void augment_methods();
void assign_slots();
@@ -666,18 +670,41 @@ registry::compiler::compiler(Options... opts)
template
template
-void registry::compiler::collect_transitive_bases(
- class_* cls, class_* base) {
- if (base->mark == class_mark) {
+void registry::compiler::calculate_transitive_bases(
+ class_& cls) {
+ if (cls.transitive_bases_done) {
return;
}
- cls->transitive_bases.push_back(base);
- base->mark = class_mark;
+ // Set before recursing. Inheritance cannot cycle, so this only guards
+ // against a malformed set of records.
+ cls.transitive_bases_done = true;
+
+ // Complete every declared base first, so that merging them below sees
+ // their full closure. Recursing bumps `class_mark`, hence the fresh mark
+ // afterwards.
+ for (auto base : cls.declared_bases) {
+ calculate_transitive_bases(*base);
+ }
+
+ auto mark = ++class_mark;
- for (auto base_base : base->transitive_bases) {
- collect_transitive_bases(cls, base_base);
+ for (auto base : cls.declared_bases) {
+ if (base->mark != mark) {
+ base->mark = mark;
+ cls.transitive_bases.push_back(base);
+ }
+
+ for (auto base_base : base->transitive_bases) {
+ if (base_base->mark != mark) {
+ base_base->mark = mark;
+ cls.transitive_bases.push_back(base_base);
+ }
+ }
}
+
+ cls.declared_bases.clear();
+ cls.declared_bases.shrink_to_fit();
}
template
@@ -760,29 +787,21 @@ void registry::compiler::augment_classes() {
if (rtc != rtb) {
// At compile time we collected the class as its own
// improper base, as per std::is_base_of. Eliminate that.
- ++class_mark;
- collect_transitive_bases(rtc, rtb);
+ rtc->declared_bases.push_back(rtb);
}
}
}
- // At this point bases may contain duplicates, and also indirect
- // bases. Clean that up.
-
- std::size_t mark = ++class_mark;
+ // `declared_bases` now holds whatever the records said - direct bases,
+ // ancestors, or a mixture, with duplicates. Turn that into the transitive
+ // closure. This is done here rather than while reading the records because
+ // a record's bases are only as informative as the records already seen: a
+ // class registered before its own bases would otherwise be left with a
+ // short list. That matters beyond `transitive_bases` itself, because the
+ // direct-base derivation below sorts on its size.
for (auto& rtc : classes) {
- decltype(rtc.transitive_bases) bases;
- mark = ++class_mark;
-
- for (auto rtb : rtc.transitive_bases) {
- if (rtb->mark != mark) {
- bases.push_back(rtb);
- rtb->mark = mark;
- }
- }
-
- rtc.transitive_bases.swap(bases);
+ calculate_transitive_bases(rtc);
}
for (auto& rtc : classes) {
@@ -793,7 +812,7 @@ void registry::compiler::augment_classes() {
[](auto a, auto b) {
return a->transitive_bases.size() > b->transitive_bases.size();
});
- mark = ++class_mark;
+ auto mark = ++class_mark;
// Collect the direct base classes. The first base is certainly a
// direct one. Remove *its* bases from the candidates, by marking
diff --git a/include/boost/openmethod/macros.hpp b/include/boost/openmethod/macros.hpp
index 4e895841..b25f0fbd 100644
--- a/include/boost/openmethod/macros.hpp
+++ b/include/boost/openmethod/macros.hpp
@@ -219,6 +219,8 @@ inline constexpr bool method_not_found = false;
//! @see [Header and Implementation Files](xref:ROOT:headers.adoc)
#define BOOST_OPENMETHOD(ID, PARAMETERS, ...) \
struct BOOST_OPENMETHOD_ID(ID); \
+ using BOOST_OPENMETHOD_GENSYM = \
+ BOOST_OPENMETHOD_TYPE(ID, PARAMETERS, __VA_ARGS__); \
template \
typename ::boost::openmethod::detail::enable_forwarder< \
void, BOOST_OPENMETHOD_TYPE(ID, PARAMETERS, __VA_ARGS__), \
@@ -587,6 +589,49 @@ inline constexpr bool method_not_found = false;
#define BOOST_OPENMETHOD_CLASSES(...) \
BOOST_OPENMETHOD_REGISTER(::boost::openmethod::use_classes<__VA_ARGS__>)
+//! Register the classes of a namespace.
+//!
+//! Finds the classes taking part in dispatch by reflection, and registers them.
+//! It makes @ref BOOST_OPENMETHOD_CLASSES unnecessary in most cases.
+//!
+//! This macro is a wrapper around @ref boost::openmethod::use_classes_in; see
+//! its documentation for more details.
+//!
+//! 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:
+//!
+//! @code
+//! struct Animal { virtual ~Animal() = default; };
+//! struct Cat : Animal {};
+//! struct Dog : Animal {};
+//! struct Bulldog : Dog {};
+//!
+//! BOOST_OPENMETHOD(poke, (std::ostream&, virtual_), void);
+//!
+//! BOOST_OPENMETHOD_OVERRIDE(poke, (std::ostream& os, Dog&), void) {
+//! os << "bark";
+//! }
+//!
+//! BOOST_OPENMETHOD_CLASSES_IN(::); // 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 nothing, so a file that also calls
+//! @ref BOOST_OPENMETHOD_CLASSES builds under either standard.
+//!
+//! @param NAMESPACE The namespace to scan, e.g. `::`.
+//! @param ... The registry, optionally.
+//!
+//! @see [Methods and Overriders](xref:ROOT:basics.adoc)
+#if BOOST_OPENMETHOD_HAS_REFLECTION
+#define BOOST_OPENMETHOD_CLASSES_IN(NAMESPACE, ...) \
+ BOOST_OPENMETHOD_REGISTER( \
+ ::boost::openmethod::use_classes_in<^^NAMESPACE __VA_OPT__(, ) \
+ __VA_ARGS__>)
+#else
+#define BOOST_OPENMETHOD_CLASSES_IN(NAMESPACE, ...) static_assert(true)
+#endif
+
// The three macros below share a registry's state - the single variable
// registry_state::st, see registry_state in preamble.hpp -
// across module boundaries, by emitting the explicit instantiations that make
diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp
index 0d53ee71..809f6a44 100644
--- a/include/boost/openmethod/preamble.hpp
+++ b/include/boost/openmethod/preamble.hpp
@@ -1,6 +1,7 @@
#ifndef BOOST_OPENMETHOD_REGISTRY_HPP
#define BOOST_OPENMETHOD_REGISTRY_HPP
+#include
#include
#include
@@ -175,6 +176,13 @@ 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_CLASSES_IN 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.
+//!
//! @see [Error Handling](xref:ROOT:error_handling.adoc)
struct missing_class : openmethod_error {
//! The type_id of the unknown class.
@@ -211,6 +219,13 @@ struct missing_class : openmethod_error {
//!
//! include:errors_missing_class_call.cpp#fix
//!
+//! @note With a compiler that supports C++26 reflection, @ref
+//! BOOST_OPENMETHOD_CLASSES_IN 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.
+//!
//! @see [Error Handling](xref:ROOT:error_handling.adoc)
struct missing_base : openmethod_error {
//! The type_id of the base class.
@@ -891,6 +906,27 @@ 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
// -----------------------------------------------------------------------------
@@ -1337,6 +1373,14 @@ 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 c404d5c1..6452506f 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -94,6 +94,7 @@ 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})
@@ -119,6 +120,7 @@ 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)
@@ -150,6 +152,7 @@ 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/Jamfile b/test/Jamfile
index 92001cde..5a588b62 100644
--- a/test/Jamfile
+++ b/test/Jamfile
@@ -46,6 +46,13 @@ project
../../type_erasure/include
BOOST_TYPE_ERASURE_NO_LIB=1
+ # C++26 reflection (P2996). GCC provides it only behind a flag, and accepts
+ # that flag only alongside a C++26 standard flag - so this is conditioned on
+ # cxxstd=26, not on cxxstd=2c, which spells the standard -std=c++2c. The
+ # library needs nothing else: it detects reflection from
+ # __cpp_impl_reflection, and falls back to explicit class registration.
+ gcc,26:-freflection
+
extra
clang:on
diff --git a/test/test_capture_errors.hpp b/test/test_capture_errors.hpp
index ddfaabee..ecf8d377 100644
--- a/test/test_capture_errors.hpp
+++ b/test/test_capture_errors.hpp
@@ -11,6 +11,13 @@
// 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
@@ -29,8 +36,15 @@ 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 b9a31c10..db2befce 100644
--- a/test/test_checked_registry.hpp
+++ b/test/test_checked_registry.hpp
@@ -15,14 +15,29 @@
//
// `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 e1f395b9..af4280f0 100644
--- a/test/test_class_registration_missing_base_class.cpp
+++ b/test/test_class_registration_missing_base_class.cpp
@@ -3,6 +3,10 @@
// 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 0781b49f..61d868a3 100644
--- a/test/test_class_registration_unknown_class_overrider.cpp
+++ b/test/test_class_registration_unknown_class_overrider.cpp
@@ -3,6 +3,10 @@
// 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
new file mode 100644
index 00000000..8947c3c4
--- /dev/null
+++ b/test/test_classes.hpp
@@ -0,0 +1,30 @@
+// 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)
+
+#ifndef BOOST_OPENMETHOD_TEST_CLASSES_HPP
+#define BOOST_OPENMETHOD_TEST_CLASSES_HPP
+
+#include
+
+//! Register classes, unless the library can find them by reflection.
+//!
+//! Expands to @ref BOOST_OPENMETHOD_CLASSES in C++17, and to nothing when the
+//! compiler supports C++26 reflection. Tests that are not *about* class
+//! registration use this instead of `BOOST_OPENMETHOD_CLASSES`, so that a
+//! C++26 run exercises reflection-based registration over the whole suite: the
+//! 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.
+
+#if BOOST_OPENMETHOD_HAS_REFLECTION
+#define BOOST_OPENMETHOD_TEST_CLASSES(...)
+#else
+#define BOOST_OPENMETHOD_TEST_CLASSES(...) BOOST_OPENMETHOD_CLASSES(__VA_ARGS__)
+#endif
+
+#endif
diff --git a/test/test_compiler.cpp b/test/test_compiler.cpp
index b4def394..95ea47b8 100644
--- a/test/test_compiler.cpp
+++ b/test/test_compiler.cpp
@@ -147,6 +147,54 @@ BOOST_AUTO_TEST_CASE(test_use_classes_linear) {
BOOST_CHECK_EQUAL(sstr(d4->transitive_derived), sstr(d4, d5));
}
+// The lattice must not depend on the order in which classes are registered.
+// Every record below carries exactly one, genuinely direct, base; only the order
+// of the calls differs from `test_use_classes_linear`, with D3's own ancestry
+// registered last.
+BOOST_AUTO_TEST_CASE(test_use_classes_derived_before_base) {
+ struct Base {
+ virtual ~Base() = default;
+ };
+
+ struct D1 : Base {};
+ struct D2 : D1 {};
+ struct D3 : D2 {};
+ struct D4 : D3 {};
+ struct D5 : D4 {};
+
+ struct registry : test_registry_<__COUNTER__> {};
+
+ BOOST_OPENMETHOD_CLASSES(D3, D4, registry);
+ BOOST_OPENMETHOD_CLASSES(D4, D5, registry);
+ BOOST_OPENMETHOD_CLASSES(Base, D1, D2, D3, registry);
+
+ auto comp = initialize();
+
+ auto base = get_class(comp);
+ auto d1 = get_class(comp);
+ auto d2 = get_class(comp);
+ auto d3 = get_class(comp);
+ auto d4 = get_class(comp);
+ auto d5 = get_class(comp);
+
+ BOOST_CHECK_EQUAL(sstr(base->direct_bases), empty);
+ BOOST_CHECK_EQUAL(sstr(d1->direct_bases), sstr(base));
+ BOOST_CHECK_EQUAL(sstr(d2->direct_bases), sstr(d1));
+ BOOST_CHECK_EQUAL(sstr(d3->direct_bases), sstr(d2));
+ BOOST_CHECK_EQUAL(sstr(d4->direct_bases), sstr(d3));
+ // D3 is an *indirect* base of D5, and must not appear here.
+ BOOST_CHECK_EQUAL(sstr(d5->direct_bases), sstr(d4));
+
+ BOOST_CHECK_EQUAL(sstr(d5->transitive_bases), sstr(base, d1, d2, d3, d4));
+ BOOST_CHECK_EQUAL(sstr(d4->transitive_bases), sstr(base, d1, d2, d3));
+
+ BOOST_CHECK_EQUAL(sstr(base->direct_derived), sstr(d1));
+ BOOST_CHECK_EQUAL(sstr(d3->direct_derived), sstr(d4));
+ BOOST_CHECK_EQUAL(sstr(d4->direct_derived), sstr(d5));
+ BOOST_CHECK_EQUAL(
+ sstr(base->transitive_derived), sstr(base, d1, d2, d3, d4, d5));
+}
+
BOOST_AUTO_TEST_CASE(test_use_classes_diamond) {
using test_registry = test_registry_<__COUNTER__>;
BOOST_OPENMETHOD_REGISTER(use_classes);
diff --git a/test/test_custom_rtti_deferred.cpp b/test/test_custom_rtti_deferred.cpp
index e56a2de2..d73be6aa 100644
--- a/test/test_custom_rtti_deferred.cpp
+++ b/test/test_custom_rtti_deferred.cpp
@@ -9,6 +9,8 @@ struct test_registry;
#include
#include
+#include "test_classes.hpp"
+
#include
namespace {
@@ -154,7 +156,7 @@ struct test_registry
using namespace boost::openmethod;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat, Bat, Owl);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat, Bat, Owl);
BOOST_OPENMETHOD(poke, (virtual_, std::ostream&), void);
@@ -233,3 +235,7 @@ BOOST_AUTO_TEST_CASE(custom_rtti_deferred) {
BOOST_TEST(os.str() == "The bat evades the owl.");
}
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_custom_rtti_simple.cpp b/test/test_custom_rtti_simple.cpp
index 98f70647..33e7d014 100644
--- a/test/test_custom_rtti_simple.cpp
+++ b/test/test_custom_rtti_simple.cpp
@@ -9,6 +9,8 @@ struct test_registry;
#include
#include
+#include "test_classes.hpp"
+
#include
namespace {
@@ -100,7 +102,7 @@ struct test_registry
using namespace boost::openmethod;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(poke, (virtual_, std::ostream&), void);
@@ -166,3 +168,7 @@ 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.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_custom_rtti_simple_projection.cpp b/test/test_custom_rtti_simple_projection.cpp
index d2f06b3b..ea1d96cb 100644
--- a/test/test_custom_rtti_simple_projection.cpp
+++ b/test/test_custom_rtti_simple_projection.cpp
@@ -9,6 +9,8 @@ struct test_registry;
#include
#include
+#include "test_classes.hpp"
+
namespace {
template
inline char non_polymorphic_static_type_storage = '\0';
@@ -86,7 +88,7 @@ struct test_registry : boost::openmethod::default_registry::with {
using namespace boost::openmethod;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(poke, (virtual_, std::ostream&), void);
@@ -114,3 +116,7 @@ BOOST_AUTO_TEST_CASE(custom_rtti_simple_projection) {
BOOST_TEST(os.str() == "Sylvester hisses.");
}
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_custom_rtti_virtual_base.cpp b/test/test_custom_rtti_virtual_base.cpp
index 0ccb51b9..e5b48ca8 100644
--- a/test/test_custom_rtti_virtual_base.cpp
+++ b/test/test_custom_rtti_virtual_base.cpp
@@ -9,6 +9,8 @@ struct test_registry;
#include
#include
+#include "test_classes.hpp"
+
#include
namespace {
@@ -125,7 +127,7 @@ struct test_registry
using namespace boost::openmethod;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(poke, (virtual_, std::ostream&), void);
@@ -191,3 +193,7 @@ 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.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_across_namespaces.cpp b/test/test_dispatch_across_namespaces.cpp
index ed1fb76d..cbcd09c4 100644
--- a/test/test_dispatch_across_namespaces.cpp
+++ b/test/test_dispatch_across_namespaces.cpp
@@ -8,6 +8,8 @@
#include
#include
+#include "test_classes.hpp"
+
#define BOOST_TEST_MODULE dispatch_across_namespaces
#include
@@ -29,7 +31,7 @@ namespace more_animals {
class Dog : public animals::Animal {};
-BOOST_OPENMETHOD_CLASSES(Dog, animals::Animal);
+BOOST_OPENMETHOD_TEST_CLASSES(Dog, animals::Animal);
BOOST_OPENMETHOD_OVERRIDE(poke, (const Dog&), std::string) {
return "bark";
@@ -43,3 +45,7 @@ BOOST_AUTO_TEST_CASE(across_namespaces) {
const animals::Animal& animal = more_animals::Dog();
BOOST_TEST("bark" == poke(animal));
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_boost_any.cpp b/test/test_dispatch_boost_any.cpp
index b3bf13b0..bea49aae 100644
--- a/test/test_dispatch_boost_any.cpp
+++ b/test/test_dispatch_boost_any.cpp
@@ -11,6 +11,8 @@
#include
#include
+#include "test_classes.hpp"
+
#define BOOST_TEST_MODULE dispatch_boost_any
#include
@@ -292,7 +294,7 @@ struct Animal {
struct Cat : Animal {};
-BOOST_OPENMETHOD_CLASSES(Animal, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Cat);
BOOST_OPENMETHOD(
meet, (virtual_, virtual_ptr),
@@ -373,7 +375,7 @@ struct Dog : Animal {
std::string name;
};
-BOOST_OPENMETHOD_CLASSES(Animal, Dog);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog);
BOOST_OPENMETHOD(poke, (virtual_ptr), std::string);
@@ -399,3 +401,7 @@ BOOST_AUTO_TEST_CASE(boost_any_class_in_hierarchy) {
BOOST_TEST(name(any_spot) == "Spot the dog");
}
} // namespace BOOST_OPENMETHOD_GENSYM
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_comma_in_return_type.cpp b/test/test_dispatch_comma_in_return_type.cpp
index 34be736f..8d77b78a 100644
--- a/test/test_dispatch_comma_in_return_type.cpp
+++ b/test/test_dispatch_comma_in_return_type.cpp
@@ -8,6 +8,8 @@
#include
#include
+#include "test_classes.hpp"
+
#define BOOST_TEST_MODULE dispatch_comma_in_return_type
#include
@@ -17,7 +19,7 @@ struct Test {
virtual ~Test(){};
};
-BOOST_OPENMETHOD_CLASSES(Test);
+BOOST_OPENMETHOD_TEST_CLASSES(Test);
BOOST_OPENMETHOD(foo, (virtual_), std::pair);
@@ -32,3 +34,7 @@ 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.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_intrusive_ptr.cpp b/test/test_dispatch_intrusive_ptr.cpp
index f22749b2..c3b3a108 100644
--- a/test/test_dispatch_intrusive_ptr.cpp
+++ b/test/test_dispatch_intrusive_ptr.cpp
@@ -12,6 +12,8 @@
#include
#include
#include
+
+#include "test_classes.hpp"
#include
#include
@@ -41,7 +43,7 @@ using namespace boost::openmethod;
using Animal::Animal; \
}; \
\
- BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+ BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
namespace BOOST_OPENMETHOD_GENSYM {
@@ -166,3 +168,7 @@ BOOST_AUTO_TEST_CASE(intrusive_virtual_ptr_by_const_ref) {
BOOST_TEST(name(felix) == "Felix the cat");
}
} // namespace BOOST_OPENMETHOD_GENSYM
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_lvalue_refs.cpp b/test/test_dispatch_lvalue_refs.cpp
index 4068e840..5c9c0dbc 100644
--- a/test/test_dispatch_lvalue_refs.cpp
+++ b/test/test_dispatch_lvalue_refs.cpp
@@ -16,7 +16,7 @@
using namespace boost::openmethod;
using namespace animals;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(name, (virtual_), std::string);
@@ -37,3 +37,7 @@ BOOST_AUTO_TEST_CASE(cast_args_lvalue_refs) {
Cat felix("Felix");
BOOST_TEST(name(felix) == "Bill's cat Felix");
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_multi.cpp b/test/test_dispatch_multi.cpp
index f1cb5a83..8a0e58a2 100644
--- a/test/test_dispatch_multi.cpp
+++ b/test/test_dispatch_multi.cpp
@@ -14,7 +14,12 @@
using namespace boost::openmethod;
using namespace test_matrices;
-BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix, diagonal_matrix);
+BOOST_OPENMETHOD_TEST_CLASSES(matrix, diagonal_matrix);
+
+// dense_matrix has no overrider of its own, and is not named in any method
+// signature, so reflection has no way of finding it. It is registered by hand
+// in C++26 too - along with its base, as use_classes requires.
+BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix);
BOOST_OPENMETHOD(
times, (virtual_, virtual_), string_pair);
@@ -69,3 +74,7 @@ BOOST_AUTO_TEST_CASE(simple) {
times(diag, 2) == string_pair(DIAGONAL_SCALAR, MATRIX_SCALAR));
}
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_next_fn.cpp b/test/test_dispatch_next_fn.cpp
index fde927d6..f3a7ce17 100644
--- a/test/test_dispatch_next_fn.cpp
+++ b/test/test_dispatch_next_fn.cpp
@@ -9,6 +9,8 @@
#include
#include
+#include "test_classes.hpp"
+
#define BOOST_TEST_MODULE dispatch_next_fn
#include
@@ -22,7 +24,7 @@ struct Animal {
struct Dog : Animal {};
struct Bulldog : Dog {};
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Bulldog);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Bulldog);
struct BOOST_OPENMETHOD_ID(poke);
using poke =
@@ -49,3 +51,7 @@ BOOST_AUTO_TEST_CASE(test_next_fn) {
std::unique_ptr hector = std::make_unique();
BOOST_TEST(poke::fn(*hector) == "bark and bite back");
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_pointer.cpp b/test/test_dispatch_pointer.cpp
index 925f3da2..7d701d5f 100644
--- a/test/test_dispatch_pointer.cpp
+++ b/test/test_dispatch_pointer.cpp
@@ -16,7 +16,7 @@
using namespace boost::openmethod;
using namespace animals;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(name, (virtual_), std::string);
@@ -37,3 +37,7 @@ BOOST_AUTO_TEST_CASE(cast_args_pointer) {
Cat felix("Felix");
BOOST_TEST(name(&felix) == "Bill's cat Felix");
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_rvalue_refs.cpp b/test/test_dispatch_rvalue_refs.cpp
index d89f8c29..39b455a4 100644
--- a/test/test_dispatch_rvalue_refs.cpp
+++ b/test/test_dispatch_rvalue_refs.cpp
@@ -16,7 +16,7 @@
using namespace boost::openmethod;
using namespace animals;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(teleport, (virtual_), std::unique_ptr);
@@ -45,3 +45,7 @@ BOOST_AUTO_TEST_CASE(cast_args_rvalue_refs) {
BOOST_TEST(felix.name == "");
}
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_shared_ptr_by_ref.cpp b/test/test_dispatch_shared_ptr_by_ref.cpp
index 6b5bb0ee..2e14030f 100644
--- a/test/test_dispatch_shared_ptr_by_ref.cpp
+++ b/test/test_dispatch_shared_ptr_by_ref.cpp
@@ -18,7 +18,7 @@
using namespace boost::openmethod;
using namespace animals;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(
name, (virtual_&>), std::string);
@@ -42,3 +42,7 @@ BOOST_AUTO_TEST_CASE(cast_args_shared_ptr_by_ref) {
auto felix = std::make_shared("Felix");
BOOST_TEST(name(felix) == "Bill's cat Felix");
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_shared_ptr_by_value.cpp b/test/test_dispatch_shared_ptr_by_value.cpp
index 064d6a99..58b9e87b 100644
--- a/test/test_dispatch_shared_ptr_by_value.cpp
+++ b/test/test_dispatch_shared_ptr_by_value.cpp
@@ -18,7 +18,7 @@
using namespace boost::openmethod;
using namespace animals;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(name, (virtual_>), std::string);
@@ -39,3 +39,7 @@ BOOST_AUTO_TEST_CASE(cast_args_shared_ptr_by_value) {
auto felix = std::make_shared("Felix");
BOOST_TEST(name(felix) == "Bill's cat Felix");
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp
index 05b68ba4..0b037ba3 100644
--- a/test/test_dispatch_std_any.cpp
+++ b/test/test_dispatch_std_any.cpp
@@ -11,6 +11,8 @@
#include
#include
+#include "test_classes.hpp"
+
#define BOOST_TEST_MODULE openmethod
#include
@@ -290,7 +292,7 @@ struct Animal {
struct Cat : Animal {};
-BOOST_OPENMETHOD_CLASSES(Animal, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Cat);
BOOST_OPENMETHOD(
meet, (virtual_, virtual_ptr), std::string);
@@ -370,7 +372,7 @@ struct Dog : Animal {
std::string name;
};
-BOOST_OPENMETHOD_CLASSES(Animal, Dog);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog);
BOOST_OPENMETHOD(poke, (virtual_ptr), std::string);
@@ -396,3 +398,7 @@ BOOST_AUTO_TEST_CASE(std_any_class_in_hierarchy) {
BOOST_TEST(name(any_spot) == "Spot the dog");
}
} // namespace BOOST_OPENMETHOD_GENSYM
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_dispatch_unique_ptr.cpp b/test/test_dispatch_unique_ptr.cpp
index 2fd4db28..c07a2f51 100644
--- a/test/test_dispatch_unique_ptr.cpp
+++ b/test/test_dispatch_unique_ptr.cpp
@@ -18,7 +18,7 @@
using namespace boost::openmethod;
using namespace animals;
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat);
BOOST_OPENMETHOD(name, (virtual_>), std::string);
@@ -41,3 +41,7 @@ BOOST_AUTO_TEST_CASE(cast_args_unique_ptr) {
BOOST_TEST(name(std::move(felix)) == "Bill's cat Felix");
BOOST_TEST(felix.get() == nullptr);
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_n2216_covariant_return_type.cpp b/test/test_n2216_covariant_return_type.cpp
index 361c15e9..6a99693d 100644
--- a/test/test_n2216_covariant_return_type.cpp
+++ b/test/test_n2216_covariant_return_type.cpp
@@ -17,7 +17,7 @@
using namespace boost::openmethod;
using namespace test_matrices;
-BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix);
+BOOST_OPENMETHOD_TEST_CLASSES(matrix, dense_matrix);
BOOST_OPENMETHOD(
times, (virtual_, virtual_),
@@ -47,3 +47,7 @@ BOOST_AUTO_TEST_CASE(covariant_return_type) {
auto result = times(left, right);
BOOST_TEST(result->type == DENSE_MATRIX);
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_n2216_pick_any_ambiguous.cpp b/test/test_n2216_pick_any_ambiguous.cpp
index dc351017..479fde3d 100644
--- a/test/test_n2216_pick_any_ambiguous.cpp
+++ b/test/test_n2216_pick_any_ambiguous.cpp
@@ -14,7 +14,7 @@
using namespace boost::openmethod;
using namespace test_matrices;
-BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix);
+BOOST_OPENMETHOD_TEST_CLASSES(matrix, dense_matrix);
BOOST_OPENMETHOD(
times, (virtual_, virtual_), string_pair);
@@ -43,3 +43,7 @@ BOOST_AUTO_TEST_CASE(pick_any_ambiguous) {
BOOST_TEST(result.first == MATRIX_DENSE);
BOOST_TEST(result.second == MATRIX_MATRIX);
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_namespaces.cpp b/test/test_namespaces.cpp
index 9273a293..e7198246 100644
--- a/test/test_namespaces.cpp
+++ b/test/test_namespaces.cpp
@@ -29,11 +29,17 @@ class Dolphin : public interfaces::Animal {};
#include
#include
+#include "test_classes.hpp"
+
using boost::openmethod::virtual_;
-BOOST_OPENMETHOD_CLASSES(
- interfaces::Animal, canis::Dog, canis::Bulldog, felis::Cat,
- delphinus::Dolphin);
+BOOST_OPENMETHOD_TEST_CLASSES(
+ interfaces::Animal, canis::Dog, canis::Bulldog, felis::Cat);
+
+// Dolphin has no overrider of its own, and is not named in any method
+// signature, so reflection has no way of finding it. It is registered by hand
+// in C++26 too - along with its base, as use_classes requires.
+BOOST_OPENMETHOD_CLASSES(interfaces::Animal, delphinus::Dolphin);
// open method with single virtual argument <=> virtual function "from outside"
BOOST_OPENMETHOD(poke, (virtual_), std::string);
@@ -102,3 +108,7 @@ auto main() -> int {
std::cout << "hector meets flipper: " << meet(*hector, *flipper)
<< "\n"; // ignore
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_pointer_to_method.cpp b/test/test_pointer_to_method.cpp
index 92138988..62bed875 100644
--- a/test/test_pointer_to_method.cpp
+++ b/test/test_pointer_to_method.cpp
@@ -6,6 +6,8 @@
#include
#include
+#include "test_classes.hpp"
+
#include
#define BOOST_TEST_MODULE openmethod
@@ -21,7 +23,7 @@ class Animal {
class Dog : public Animal {};
-BOOST_OPENMETHOD_CLASSES(Animal, Dog, Animal);
+BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Animal);
BOOST_OPENMETHOD(poke, (virtual_), std::string);
@@ -35,3 +37,7 @@ BOOST_AUTO_TEST_CASE(noadl) {
Dog snoopy;
BOOST_TEST(stimulus(snoopy) == "bark");
}
+
+// Registers the classes above by reflection, when the compiler supports it.
+// Must come last: reflection sees only what precedes it.
+BOOST_OPENMETHOD_CLASSES_IN(::);
diff --git a/test/test_reflection.cpp b/test/test_reflection.cpp
new file mode 100644
index 00000000..4ed91aac
--- /dev/null
+++ b/test/test_reflection.cpp
@@ -0,0 +1,566 @@
+// 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)
+
+// Tests for reflection-based class registration - `use_classes_in` and
+// `BOOST_OPENMETHOD_CLASSES_IN`. Without a compiler that supports C++26
+// reflection there is nothing to test, and the whole file reduces to one test
+// case that says so.
+
+#include
+#include
+
+#include
+#include
+
+#include "test_util.hpp"
+
+#define BOOST_TEST_MODULE reflection
+#include
+
+#if !BOOST_OPENMETHOD_HAS_REFLECTION
+
+BOOST_AUTO_TEST_CASE(reflection_not_supported) {
+ BOOST_TEST_MESSAGE("compiler does not support C++26 reflection");
+}
+
+#else
+
+#include
+
+#include
+#include
+
+using namespace boost::openmethod;
+
+// True if `Class` was registered in `Registry`, as seen by the compiler object
+// `initialize` returns.
+template
+auto registered(const Compiler& comp) -> bool {
+ return comp.class_map.find(Registry::rtti::type_index(
+ Registry::rtti::template static_type())) !=
+ comp.class_map.end();
+}
+
+// =============================================================================
+// The macro interface, and a leaf class no signature names
+
+namespace macro_interface {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+struct Animal {
+ virtual ~Animal() = default;
+};
+
+struct Dog : Animal {};
+struct Cat : Animal {};
+
+// Never named in a method or an overrider, and never wrapped in a virtual_ptr.
+// Only the namespace scan can find it.
+struct Bulldog : Dog {};
+
+// Not related to any virtual parameter: must be left alone.
+struct Fence {
+ virtual ~Fence() = default;
+};
+
+BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Animal&), std::string) {
+ return "generic";
+}
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), std::string) {
+ return "bark";
+}
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Cat&), std::string) {
+ return "hiss";
+}
+
+} // namespace macro_interface
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<^^macro_interface, macro_interface::test_registry>);
+
+BOOST_AUTO_TEST_CASE(macro_interface_dispatch) {
+ using namespace macro_interface;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ // Found by the scan, although nothing dispatches on it.
+ BOOST_TEST((registered(comp)));
+ // Unrelated to every virtual parameter.
+ BOOST_TEST((!registered(comp)));
+
+ Animal animal;
+ Dog dog;
+ Cat cat;
+ Bulldog bulldog;
+
+ BOOST_TEST(poke(animal) == "generic");
+ BOOST_TEST(poke(dog) == "bark");
+ BOOST_TEST(poke(cat) == "hiss");
+ // Bulldog has no overrider of its own; it is registered as derived from
+ // Dog, so Dog's overrider applies.
+ BOOST_TEST(poke(bulldog) == "bark");
+}
+
+// =============================================================================
+// The core interface: a method named by an alias, overriders as free functions
+
+namespace core_interface {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+struct Node {
+ virtual ~Node() = default;
+};
+
+struct Literal : Node {};
+struct Plus : Node {};
+
+struct BOOST_OPENMETHOD_ID(value);
+
+using value = method<
+ BOOST_OPENMETHOD_ID(value), std::string(virtual_ptr),
+ test_registry>;
+
+auto value_literal(virtual_ptr) -> std::string {
+ return "literal";
+}
+
+auto value_plus(virtual_ptr) -> std::string {
+ return "plus";
+}
+
+BOOST_OPENMETHOD_REGISTER(value::override);
+
+} // namespace core_interface
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<^^core_interface, core_interface::test_registry>);
+
+BOOST_AUTO_TEST_CASE(core_interface_dispatch) {
+ using namespace core_interface;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+
+ Literal literal;
+ Plus plus;
+
+ BOOST_TEST(
+ value::fn(virtual_ptr(literal)) == "literal");
+ BOOST_TEST(value::fn(virtual_ptr(plus)) == "plus");
+}
+
+// =============================================================================
+// A method with no overrider at all
+
+namespace method_only {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+struct Animal {
+ virtual ~Animal() = default;
+};
+
+struct Dog : Animal {};
+
+// No overrider, so no registrar object names the method. It is found through
+// the alias BOOST_OPENMETHOD declares for it.
+BOOST_OPENMETHOD(poke, (virtual_), void, test_registry);
+
+} // namespace method_only
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<^^method_only, method_only::test_registry>);
+
+BOOST_AUTO_TEST_CASE(method_without_overrider) {
+ using namespace method_only;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+}
+
+// =============================================================================
+// Inheritance: virtual, multiple, and inaccessible bases
+
+namespace inheritance {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+struct Animal {
+ virtual ~Animal() = default;
+};
+
+struct Herbivore : virtual Animal {};
+struct Carnivore : virtual Animal {};
+struct Omnivore : Herbivore, Carnivore {};
+
+// Reached only through a private base: the library cannot convert a Stowaway to
+// an Animal, so it must not be registered as one.
+struct Stowaway : private Animal {};
+
+BOOST_OPENMETHOD(
+ meet, (virtual_, virtual_), std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(meet, (Animal&, Animal&), std::string) {
+ return "ignore";
+}
+
+BOOST_OPENMETHOD_OVERRIDE(meet, (Carnivore&, Herbivore&), std::string) {
+ return "hunt";
+}
+
+} // namespace inheritance
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<^^inheritance, inheritance::test_registry>);
+
+BOOST_AUTO_TEST_CASE(virtual_and_multiple_inheritance) {
+ using namespace inheritance;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((!registered(comp)));
+
+ Herbivore herbivore;
+ Carnivore carnivore;
+ Omnivore omnivore;
+
+ BOOST_TEST(meet(herbivore, herbivore) == "ignore");
+ BOOST_TEST(meet(carnivore, herbivore) == "hunt");
+ // Omnivore is both, and inherits the Carnivore/Herbivore overrider.
+ BOOST_TEST(meet(omnivore, omnivore) == "hunt");
+}
+
+// =============================================================================
+// Repeated inheritance is left out, not rejected
+//
+// `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.
+
+namespace repeated_inheritance {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+struct Animal {
+ virtual ~Animal() = default;
+};
+
+struct Dog : Animal {};
+
+struct Left : Animal {};
+struct Right : Animal {};
+// Animal is an ambiguous base: no conversion to it exists.
+struct Repeated : Left, Right {};
+
+BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Animal&), std::string) {
+ return "generic";
+}
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), std::string) {
+ return "bark";
+}
+
+} // namespace repeated_inheritance
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<
+ ^^repeated_inheritance, repeated_inheritance::test_registry>);
+
+BOOST_AUTO_TEST_CASE(repeated_inheritance_does_not_break_the_scan) {
+ using namespace repeated_inheritance;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+
+ Dog dog;
+ Left left;
+ BOOST_TEST(poke(dog) == "bark");
+ BOOST_TEST(poke(left) == "generic");
+}
+
+// =============================================================================
+// Nested namespaces, smart pointers, and covariant return types
+
+namespace nested {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+namespace shapes {
+
+struct Shape {
+ virtual ~Shape() = default;
+};
+
+namespace round {
+struct Circle : Shape {};
+} // namespace round
+
+} // namespace shapes
+
+BOOST_OPENMETHOD(
+ name, (virtual_ptr, test_registry>),
+ std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(
+ name, (virtual_ptr, test_registry>),
+ std::string) {
+ return "circle";
+}
+
+} // namespace nested
+
+BOOST_OPENMETHOD_REGISTER(use_classes_in<^^nested, nested::test_registry>);
+
+BOOST_AUTO_TEST_CASE(nested_namespaces_and_smart_pointers) {
+ using namespace nested;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+
+ std::shared_ptr circle =
+ std::make_shared();
+ BOOST_TEST(
+ name(virtual_ptr, test_registry>(
+ circle)) == "circle");
+}
+
+// =============================================================================
+// A base class nothing dispatches on is not registered
+//
+// Registering it would cost a class_info, a perfect-hash slot and dispatch table
+// space, for a class no overrider can ever be selected on.
+
+namespace unused_bases {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+// Neither of these is a virtual parameter of any method below.
+struct Serializable {
+ virtual ~Serializable() = default;
+};
+
+struct Named : Serializable {};
+
+struct Animal : Named {};
+struct Dog : Animal {};
+
+BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Animal&), std::string) {
+ return "generic";
+}
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), std::string) {
+ return "bark";
+}
+
+} // namespace unused_bases
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<^^unused_bases, unused_bases::test_registry>);
+
+BOOST_AUTO_TEST_CASE(bases_that_take_no_part_in_dispatch_are_left_out) {
+ using namespace unused_bases;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ // Above the only virtual parameter, so of no use to dispatch.
+ BOOST_TEST((!registered(comp)));
+ BOOST_TEST((!registered(comp)));
+
+ Animal animal;
+ Dog dog;
+ BOOST_TEST(poke(animal) == "generic");
+ BOOST_TEST(poke(dog) == "bark");
+}
+
+// =============================================================================
+// ... unless another method dispatches on it
+
+namespace shared_bases {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+struct Serializable {
+ virtual ~Serializable() = default;
+};
+
+struct Named : Serializable {};
+
+struct Animal : Named {};
+struct Dog : Animal {};
+
+BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (Dog&), std::string) {
+ return "bark";
+}
+
+// Named is a virtual parameter here, so it - and the lattice edges through it -
+// must be registered after all.
+BOOST_OPENMETHOD(label, (virtual_), std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(label, (Named&), std::string) {
+ return "named";
+}
+
+BOOST_OPENMETHOD_OVERRIDE(label, (Dog&), std::string) {
+ return "dog";
+}
+
+} // namespace shared_bases
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<^^shared_bases, shared_bases::test_registry>);
+
+BOOST_AUTO_TEST_CASE(a_base_another_method_dispatches_on_is_registered) {
+ using namespace shared_bases;
+
+ auto comp = initialize();
+
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ BOOST_TEST((registered(comp)));
+ // Still above every virtual parameter.
+ BOOST_TEST((!registered(comp)));
+
+ Animal animal;
+ Dog dog;
+ BOOST_TEST(poke(dog) == "bark");
+ // Dog reaches Named through Animal: the edges survive.
+ BOOST_TEST(label(dog) == "dog");
+ BOOST_TEST(label(animal) == "named");
+}
+
+// =============================================================================
+// The recorded bases are the direct ones
+//
+// 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.
+
+namespace direct_bases {
+
+struct test_registry : test_registry_<__COUNTER__> {};
+
+struct A {
+ virtual ~A() = default;
+};
+
+struct B : A {};
+struct C : B {};
+struct D : C {};
+
+BOOST_OPENMETHOD(poke, (virtual_), std::string, test_registry);
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (A&), std::string) {
+ return "A";
+}
+
+BOOST_OPENMETHOD_OVERRIDE(poke, (C&), std::string) {
+ return "C";
+}
+
+} // namespace direct_bases
+
+BOOST_OPENMETHOD_REGISTER(
+ use_classes_in<^^direct_bases, direct_bases::test_registry>);
+
+BOOST_AUTO_TEST_CASE(recorded_bases_are_direct) {
+ 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.
+ std::size_t recorded = 0;
+
+ for (auto iter = comp.classes_begin(); iter != comp.classes_end(); ++iter) {
+ recorded += iter->last_base - iter->first_base;
+ }
+
+ BOOST_TEST(recorded == 7u); // A: 1, B/C/D: 2 each
+
+ // The lattice initialize derives from them is still the full chain.
+ auto a = comp.class_map.at(
+ test_registry::rtti::type_index(test_registry::rtti::static_type()));
+ auto d = comp.class_map.at(
+ test_registry::rtti::type_index(test_registry::rtti::static_type()));
+
+ BOOST_TEST(d->direct_bases.size() == 1u);
+ BOOST_TEST(d->transitive_bases.size() == 3u);
+ BOOST_TEST(a->transitive_derived.size() == 4u);
+
+ A a_obj;
+ B b;
+ C c;
+ D d_obj;
+ BOOST_TEST(poke(a_obj) == "A");
+ BOOST_TEST(poke(b) == "A");
+ BOOST_TEST(poke(c) == "C");
+ 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(
+ use_classes_in<^^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);
+}
+
+#endif
diff --git a/test/test_rolex.cpp b/test/test_rolex.cpp
index 4bef424b..0079ade8 100644
--- a/test/test_rolex.cpp
+++ b/test/test_rolex.cpp
@@ -6,6 +6,8 @@
#include
#include