From 45ec3c22d7ff711e89677054dd0ed8641880379f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 12 Sep 2026 11:00:14 -0400 Subject: [PATCH 1/2] initialize: the class range is an input range, not a forward one `const_class_iterator` declared `forward_iterator_tag` while its `reference` is `class_view` - a prvalue built on the fly by `operator*`. A forward iterator owes a real reference and a multipass guarantee; this one can give neither: two iterators at the same position hand out views at different addresses. libstdc++ says so outright under `_GLIBCXX_CONCEPT_CHECKS`, which rejects an algorithm asserting `_ForwardIteratorConcept` over the range with "reference type of a forward iterator must be a real reference". Nothing in the tree acted on the promise - `fast_perfect_hash`, `vptr_vector` and `vptr_map` only ever `++`, `!=` and dereference, and `std::distance` needs no more - but `classes_begin()`/`classes_end()` are public API, so an out-of-tree policy author is entitled to. Say input, and say in the blueprint why. Not a regression: before the class range was reshaped, the iterator advertised the same tag with `reference` spelled `const class_info*&` while `operator*` returned a prvalue, so even `std::iterator_traits::reference r = *it;` did not compile. This is a long-standing mis-tag, now corrected. The `type_id_begin()`/`type_id_end()` blueprint still says forward, correctly: those return a pointer. test_initialize_context.cpp pins the category, and the reason for it - that `reference` and `value_type` are the same type. It fails the first assertion if the tag goes back to forward. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JQa4fuiwcfsheZYTCyfPPr --- include/boost/openmethod/initialize.hpp | 9 +- include/boost/openmethod/preamble.hpp | 7 +- test/test_initialize_context.cpp | 109 ++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 test/test_initialize_context.cpp diff --git a/include/boost/openmethod/initialize.hpp b/include/boost/openmethod/initialize.hpp index 7f4c4e8c..d9bd4a88 100644 --- a/include/boost/openmethod/initialize.hpp +++ b/include/boost/openmethod/initialize.hpp @@ -355,7 +355,14 @@ struct generic_compiler { } }; - using iterator_category = std::forward_iterator_tag; + // Input, not forward: `reference` is `class_view`, a prvalue made on + // the fly, so two iterators at the same position hand out views at + // different addresses. That fails the forward-iterator requirement + // that `reference` be a real reference - libstdc++ says so outright + // under _GLIBCXX_CONCEPT_CHECKS - and the multipass guarantee with it. + // Everything a policy needs (`++`, `!=`, `*`, `std::distance`) is an + // input-iterator operation. + using iterator_category = std::input_iterator_tag; using value_type = class_view; using difference_type = std::ptrdiff_t; using pointer = arrow_proxy; diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 0a576d3d..76174b57 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -545,13 +545,14 @@ struct InitializeClass { struct InitializeContext { //! Beginning of a range of `InitializeClass` objects. //! - //! @return A forward iterator to the beginning of a range of @ref - //! InitializeClass objects. + //! @return An input iterator to the beginning of a range of @ref + //! InitializeClass objects. It is not a forward iterator: dereferencing it + //! yields a value, not a reference, so the range is single-pass. detail::unspecified classes_begin() const; //! End of a range of `InitializeClass` objects. //! - //! @return A forward iterator to the end of a range of @ref + //! @return An input iterator to the end of a range of @ref //! InitializeClass objects. detail::unspecified classes_end() const; }; diff --git a/test/test_initialize_context.cpp b/test/test_initialize_context.cpp new file mode 100644 index 00000000..f6980860 --- /dev/null +++ b/test/test_initialize_context.cpp @@ -0,0 +1,109 @@ +// 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) + +// The class range a policy's `initialize` receives is an *input* range. Its +// iterator makes the `InitializeClass` view on the fly, so dereferencing +// yields a value, not a reference: two iterators at the same position hand out +// distinct views, and the multipass guarantee a forward iterator owes does not +// hold. It used to advertise `forward_iterator_tag` anyway, which is a promise +// a generic algorithm is entitled to act on. +// +// Pinned here because nothing else in the tree looks at the category: the +// library's own policies only ever `++`, `!=` and dereference. + +#include +#include + +#define BOOST_TEST_MODULE initialize_context +#include + +#include "test_util.hpp" + +#include +#include +#include +#include + +using namespace boost::openmethod; + +// Rides along on a registry that keeps its usual vptr policy, purely to get +// hold of a Context. +struct context_checks { + using category = context_checks; + + template + struct fn { + struct state { + std::size_t classes_seen = 0; + bool values_stable = false; + }; + + template + static void initialize( + const Context& ctx, const std::tuple&) { + using iterator = decltype(ctx.classes_begin()); + using traits = std::iterator_traits; + + static_assert(std::is_same_v< + typename traits::iterator_category, + std::input_iterator_tag>); + + // Why it cannot be a forward iterator: `reference` is the value + // type, so there is nothing for a second pass to refer back to. + static_assert( + std::is_same_v< + typename traits::reference, typename traits::value_type>); + + auto& st = Registry::template state(); + + st.classes_seen = static_cast( + std::distance(ctx.classes_begin(), ctx.classes_end())); + + // Single-pass does not mean unstable: two iterators at the same + // position describe the same class, even though they hand out + // separate views of it. + auto i = ctx.classes_begin(); + auto j = ctx.classes_begin(); + st.values_stable = i != ctx.classes_end() && + i->vptr() == j->vptr() && i->static_vptr() == j->static_vptr(); + } + }; +}; + +template +struct test_reg : test_registry_::template with {}; + +using reg = test_reg<__COUNTER__>; + +struct Animal { + virtual ~Animal() = default; +}; + +struct Dog : Animal {}; +struct Cat : Animal {}; + +struct BOOST_OPENMETHOD_ID(poke); + +using poke = method< + BOOST_OPENMETHOD_ID(poke), auto(virtual_)->std::string, reg>; + +auto poke_animal(Animal&) -> std::string { + return "silence"; +} + +BOOST_AUTO_TEST_CASE(the_class_range_is_an_input_range) { + BOOST_OPENMETHOD_REGISTER(use_classes); + BOOST_OPENMETHOD_REGISTER(poke::override); + + initialize(); + + auto& st = reg::state(); + BOOST_TEST(st.classes_seen == 3u); + BOOST_TEST(st.values_stable); + + // And the registry works, so the policy really did run. + Dog dog; + BOOST_TEST(poke::fn(dog) == "silence"); +} From 5f14728d8060ca6c64a194b1958f57a2d865bbd4 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Sat, 12 Sep 2026 11:00:24 -0400 Subject: [PATCH 2/2] preamble: drop class_info::vptr(), a misleading homonym `detail::class_info::vptr()` returned `*static_vptr` - the v-table pointer from the *previous* initialize(), by reference. `class_view::vptr()`, which is what a policy sees, returns the one being *staged*, by value. The two differ for every class on every pass, and differ precisely while the policies run, which is when a reader of initialize.hpp is most likely to reach for the wrong one. One caller was left, a trace line that took its address - so `&cr.vptr()` was a two-step spelling of `cr.static_vptr`, correct but roundabout. Say `cr.static_vptr` and delete the accessor. No behaviour change: the address of `*static_vptr` is `static_vptr`. Nothing outside include/ ever used it, and `class_info` is never handed to a policy - the InitializeContext blueprint exposes only `class_view` - so the confusion was confined to future edits inside the library. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JQa4fuiwcfsheZYTCyfPPr --- include/boost/openmethod/initialize.hpp | 2 +- include/boost/openmethod/preamble.hpp | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/include/boost/openmethod/initialize.hpp b/include/boost/openmethod/initialize.hpp index d9bd4a88..4d5c96c0 100644 --- a/include/boost/openmethod/initialize.hpp +++ b/include/boost/openmethod/initialize.hpp @@ -897,7 +897,7 @@ void registry::compiler::augment_classes() { indent _(tr); ++tr << type_name(cr.type) << ": " << range{cr.first_base, cr.last_base} - << ", type = " << cr.type << ", &vptr = " << &cr.vptr() + << ", type = " << cr.type << ", &vptr = " << cr.static_vptr << "\n"; } diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 76174b57..cbf78226 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -319,10 +319,6 @@ struct class_info : static_list::static_link { type_id *first_base, *last_base; bool is_abstract{false}; - auto vptr() const -> const vptr_type& { - return *static_vptr; - } - auto type_id_begin() const { return &type; }