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 +#include "test_classes.hpp" + #define BOOST_TEST_MODULE test_rolex #include @@ -37,7 +39,7 @@ struct Metro : Public {}; struct Taxi : Expense {}; struct PrivateJet : Expense {}; -BOOST_OPENMETHOD_CLASSES( +BOOST_OPENMETHOD_TEST_CLASSES( Role, Employee, Manager, Founder, Expense, Public, Bus, Metro, Taxi, PrivateJet); @@ -184,3 +186,7 @@ BOOST_AUTO_TEST_CASE(approve_via_wrapper) { BOOST_TEST(call_approve(m, taxi, 10) == true); BOOST_TEST(call_approve(f, taxi, 10) == true); } + +// 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_runtime_errors_bad_call.cpp b/test/test_runtime_errors_bad_call.cpp index 1cb696d1..a203491c 100644 --- a/test/test_runtime_errors_bad_call.cpp +++ b/test/test_runtime_errors_bad_call.cpp @@ -17,7 +17,7 @@ using namespace test_matrices; using capture = capture_errors; -BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix, diagonal_matrix); +BOOST_OPENMETHOD_TEST_CLASSES(matrix, dense_matrix, diagonal_matrix); BOOST_OPENMETHOD( times, (virtual_ptr, virtual_ptr), void); @@ -53,3 +53,7 @@ BOOST_AUTO_TEST_CASE(bad_calls) { BOOST_TEST(capture().find("ambiguous") != std::string::npos); } } + +// 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_runtime_errors_bad_call_type_ids.cpp b/test/test_runtime_errors_bad_call_type_ids.cpp index 00df1707..9707d7fa 100644 --- a/test/test_runtime_errors_bad_call_type_ids.cpp +++ b/test/test_runtime_errors_bad_call_type_ids.cpp @@ -17,7 +17,7 @@ using namespace test_matrices; using capture = capture_errors; -BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix, diagonal_matrix); +BOOST_OPENMETHOD_TEST_CLASSES(matrix, dense_matrix, diagonal_matrix); BOOST_OPENMETHOD( times, (virtual_ptr, virtual_ptr), void); @@ -48,3 +48,7 @@ BOOST_AUTO_TEST_CASE(bad_call_type_ids) { BOOST_FAIL("wrong exception"); } } + +// 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_runtime_errors_bad_call_type_ids_smart_ptr.cpp b/test/test_runtime_errors_bad_call_type_ids_smart_ptr.cpp index 3bbe4292..5d48f077 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 @@ -18,7 +18,7 @@ using namespace test_matrices; using capture = capture_errors; -BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix, diagonal_matrix); +BOOST_OPENMETHOD_TEST_CLASSES(matrix, dense_matrix, diagonal_matrix); BOOST_OPENMETHOD( times, (shared_virtual_ptr, shared_virtual_ptr), @@ -41,3 +41,7 @@ BOOST_AUTO_TEST_CASE(bad_call_type_ids_smart_ptr) { BOOST_FAIL("wrong exception"); } } + +// 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_runtime_errors_duplicate_overrider.cpp b/test/test_runtime_errors_duplicate_overrider.cpp index f387bf7f..72f618ab 100644 --- a/test/test_runtime_errors_duplicate_overrider.cpp +++ b/test/test_runtime_errors_duplicate_overrider.cpp @@ -7,6 +7,8 @@ #include +#include "test_classes.hpp" + #define BOOST_TEST_MODULE runtime_errors_duplicate_overrider #include @@ -21,7 +23,7 @@ struct Animal { struct Dog : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Dog); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); BOOST_OPENMETHOD(poke, (virtual_ptr), const char*); @@ -58,3 +60,7 @@ BOOST_AUTO_TEST_CASE(duplicate_overrider_is_ambiguous) { BOOST_CHECK_THROW(poke(dog), ambiguous_call); BOOST_TEST(capture().find("ambiguous") != std::string::npos); } + +// 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_runtime_errors_initialize_unknown_class.cpp b/test/test_runtime_errors_initialize_unknown_class.cpp index 1dc50856..cb3e4f92 100644 --- a/test/test_runtime_errors_initialize_unknown_class.cpp +++ b/test/test_runtime_errors_initialize_unknown_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_capture_errors.hpp" #include diff --git a/test/test_runtime_errors_throw_error.cpp b/test/test_runtime_errors_throw_error.cpp index 81d9ea33..7e2de94f 100644 --- a/test/test_runtime_errors_throw_error.cpp +++ b/test/test_runtime_errors_throw_error.cpp @@ -21,7 +21,7 @@ struct test_registry : boost::openmethod::default_registry::with< using namespace boost::openmethod; using namespace test_matrices; -BOOST_OPENMETHOD_CLASSES(matrix, dense_matrix, diagonal_matrix); +BOOST_OPENMETHOD_TEST_CLASSES(matrix, dense_matrix, diagonal_matrix); BOOST_OPENMETHOD( times, (virtual_, virtual_), void); @@ -40,3 +40,7 @@ BOOST_AUTO_TEST_CASE(throw_error) { BOOST_FAIL("wrong exception"); } } + +// 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_smart_virtual_ptr_value_semantics.cpp b/test/test_smart_virtual_ptr_value_semantics.cpp index 528bfcda..a5764d96 100644 --- a/test/test_smart_virtual_ptr_value_semantics.cpp +++ b/test/test_smart_virtual_ptr_value_semantics.cpp @@ -443,3 +443,7 @@ 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. +// Must come last: reflection sees only what precedes it. +BOOST_OPENMETHOD_CLASSES_IN(::); diff --git a/test/test_static_rtti.cpp b/test/test_static_rtti.cpp index 536a03dd..4bc574b8 100644 --- a/test/test_static_rtti.cpp +++ b/test/test_static_rtti.cpp @@ -17,6 +17,8 @@ struct static_registry #define BOOST_TEST_MODULE openmethod #include +#include "test_classes.hpp" + struct Animal {}; struct Dog : Animal {}; @@ -25,7 +27,7 @@ struct Cat : Animal {}; using namespace boost::openmethod::aliases; -BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); BOOST_OPENMETHOD(poke, (virtual_ptr, std::ostream&), void); @@ -54,3 +56,7 @@ BOOST_AUTO_TEST_CASE(static_rtti) { BOOST_TEST(os.str() == "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_util.hpp b/test/test_util.hpp index ab428b22..ffda7940 100644 --- a/test/test_util.hpp +++ b/test/test_util.hpp @@ -13,6 +13,8 @@ #include #include +#include "test_classes.hpp" + struct unique_category { using category = unique_category; }; diff --git a/test/test_virtual_any_boost.cpp b/test/test_virtual_any_boost.cpp index 3672cedb..dee7a020 100644 --- a/test/test_virtual_any_boost.cpp +++ b/test/test_virtual_any_boost.cpp @@ -12,6 +12,8 @@ #include #include +#include "test_classes.hpp" + #define BOOST_TEST_MODULE openmethod #include @@ -308,7 +310,7 @@ struct Animal { struct Cat : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Cat); BOOST_OPENMETHOD( meet, (const virtual_boost_any&, virtual_ptr), std::string); @@ -334,3 +336,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { BOOST_TEST(meet(pi, felix) == "someone meets an animal"); } } // 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_virtual_any_std.cpp b/test/test_virtual_any_std.cpp index bc96172f..4e371ab4 100644 --- a/test/test_virtual_any_std.cpp +++ b/test/test_virtual_any_std.cpp @@ -12,6 +12,8 @@ #include #include +#include "test_classes.hpp" + #define BOOST_TEST_MODULE openmethod #include @@ -308,7 +310,7 @@ struct Animal { struct Cat : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Cat); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Cat); BOOST_OPENMETHOD( meet, (const virtual_std_any&, virtual_ptr), std::string); @@ -334,3 +336,7 @@ BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { BOOST_TEST(meet(pi, felix) == "someone meets an animal"); } } // 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_virtual_ptr_by_ref.cpp b/test/test_virtual_ptr_by_ref.cpp index 83a030a8..087a8910 100644 --- a/test/test_virtual_ptr_by_ref.cpp +++ b/test/test_virtual_ptr_by_ref.cpp @@ -6,6 +6,8 @@ #include #include +#include "test_classes.hpp" + #include #define BOOST_TEST_MODULE virtual_ptr_by_ref @@ -21,7 +23,7 @@ struct Animal { struct Dog : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Dog); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); BOOST_OPENMETHOD(poke, (const virtual_ptr&, std::ostream&), void); @@ -60,3 +62,7 @@ BOOST_AUTO_TEST_CASE(test_virtual_ptr_by_ref) { BOOST_CHECK(os.is_equal("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_virtual_ptr_non_polymorphic.cpp b/test/test_virtual_ptr_non_polymorphic.cpp index 932cd0a9..995e00c3 100644 --- a/test/test_virtual_ptr_non_polymorphic.cpp +++ b/test/test_virtual_ptr_non_polymorphic.cpp @@ -6,6 +6,8 @@ #include #include +#include "test_classes.hpp" + #include #define BOOST_TEST_MODULE virtual_ptr_non_polymorphic @@ -18,7 +20,7 @@ struct Animal {}; struct Dog : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Dog); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); BOOST_OPENMETHOD(poke, (virtual_ptr, std::ostream&), void); @@ -37,3 +39,7 @@ BOOST_AUTO_TEST_CASE(test_virtual_ptr_non_polymorphic) { BOOST_CHECK(os.is_equal("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_virtual_ptr_shared_by_const_ref.cpp b/test/test_virtual_ptr_shared_by_const_ref.cpp index a902d983..0ca159e7 100644 --- a/test/test_virtual_ptr_shared_by_const_ref.cpp +++ b/test/test_virtual_ptr_shared_by_const_ref.cpp @@ -7,6 +7,8 @@ #include #include +#include "test_classes.hpp" + #include #define BOOST_TEST_MODULE virtual_ptr_shared_by_const_ref @@ -22,7 +24,7 @@ struct Animal { struct Dog : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Dog); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); BOOST_OPENMETHOD( poke, (const shared_virtual_ptr&, std::ostream&), void); @@ -42,3 +44,7 @@ BOOST_AUTO_TEST_CASE(test_virtual_shared_by_const_reference) { BOOST_CHECK(os.is_equal("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_virtual_ptr_shared_by_value.cpp b/test/test_virtual_ptr_shared_by_value.cpp index 66db0f93..f37bf8cf 100644 --- a/test/test_virtual_ptr_shared_by_value.cpp +++ b/test/test_virtual_ptr_shared_by_value.cpp @@ -7,6 +7,8 @@ #include #include +#include "test_classes.hpp" + #include #define BOOST_TEST_MODULE virtual_ptr_shared_by_value @@ -22,7 +24,7 @@ struct Animal { struct Dog : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Dog); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); BOOST_OPENMETHOD(poke, (virtual_ptr, std::ostream&), void); @@ -40,3 +42,7 @@ BOOST_AUTO_TEST_CASE(test_virtual_shared_by_value) { BOOST_CHECK(os.is_equal("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_virtual_ptr_unique.cpp b/test/test_virtual_ptr_unique.cpp index 73dba04a..f6d42f5c 100644 --- a/test/test_virtual_ptr_unique.cpp +++ b/test/test_virtual_ptr_unique.cpp @@ -7,6 +7,8 @@ #include #include +#include "test_classes.hpp" + #include #define BOOST_TEST_MODULE virtual_ptr_unique @@ -22,7 +24,7 @@ struct Animal { struct Dog : Animal {}; -BOOST_OPENMETHOD_CLASSES(Animal, Dog); +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog); BOOST_OPENMETHOD(poke, (unique_virtual_ptr, std::ostream&), void); @@ -40,3 +42,7 @@ BOOST_AUTO_TEST_CASE(test_virtual_unique) { BOOST_CHECK(os.is_equal("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_virtual_ptr_value_semantics.cpp b/test/test_virtual_ptr_value_semantics.cpp index c4c5a1dd..01e9c33a 100644 --- a/test/test_virtual_ptr_value_semantics.cpp +++ b/test/test_virtual_ptr_value_semantics.cpp @@ -274,7 +274,7 @@ BOOST_AUTO_TEST_CASE_TEMPLATE(indirect_virtual_ptr, Registry, test_policies) { // Add a class, to make sure dispatch data is not re-constructed in the same // place with the same values: struct Cat : Animal {}; - BOOST_OPENMETHOD_CLASSES(Animal, Cat, Registry); + BOOST_OPENMETHOD_TEST_CLASSES(Animal, Cat, Registry); init_test(); diff --git a/test/test_virtual_ptr_value_semantics.hpp b/test/test_virtual_ptr_value_semantics.hpp index 0f630c57..7e842142 100644 --- a/test/test_virtual_ptr_value_semantics.hpp +++ b/test/test_virtual_ptr_value_semantics.hpp @@ -37,6 +37,9 @@ struct Cat : virtual Animal {}; struct Dog : Animal {}; +// These tests exercise virtual_ptr itself and declare no method, so there is no +// virtual parameter for reflection to start from: the classes are registered by +// hand under every standard. BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog); struct id;