virtual_ptr: allow a virtual_ptr to the class as a member of the class - #109
Merged
Merged
Conversation
struct Node {
virtual ~Node() = default;
virtual_ptr<Node> next;
};
failed to compile with gcc 16: `invalid use of incomplete type 'struct
Node'`, from `std::is_polymorphic_v<Node>`. Three member templates of
`virtual_ptr` - the constructor from `Other*`, and the assignments from
`Other&` and `Other*` - constrained themselves with
`IsPolymorphic<Class, Registry> && std::is_constructible_v<Class*, Other*>`
in a default template argument. The first operand does not depend on `Other`,
so gcc evaluates it when the class itself is instantiated - for the member
declaration, where `Node` is still incomplete. libstdc++ 13 answered
`is_polymorphic` on an incomplete type; 16 rejects it, as the standard allows.
The check now names `Other`, through an ignored trailing pack on the
exposition-only `IsPolymorphic`, which makes it dependent and defers it to the
first use of the member - where the class is complete. And it sits in a
defaulted parameter of its own, after the pointer-convertibility test: clang
reaches these candidates during overload resolution for the member's implicit
copy assignment, with `Other` = `const virtual_ptr<Node>`, and substitution
stops at the first condition that fails. That is the shape CLAUDE.md
prescribes for MrDocs anyway, and the rendered constraint reads the same.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDgWtJsC4KihJHq73cuCcV
|
An automated preview of the documentation is available at https://109.openmethod.prtest3.cppalliance.org/libs/openmethod/doc/html/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-09-13 17:27:11 UTC |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #109 +/- ##
===========================================
- Coverage 93.62% 93.51% -0.12%
===========================================
Files 22 22
Lines 1694 1695 +1
Branches 505 504 -1
===========================================
- Hits 1586 1585 -1
- Misses 64 66 +2
Partials 44 44
... and 1 file with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
jll63
added a commit
to jll63/openmethod
that referenced
this pull request
Sep 15, 2026
…ered Review of this branch turned up five ways `registry_affinity` gives a wrong or unchecked answer, each reproduced on gcc 13, clang 18 and MSVC. **Memoization.** `registry_affinity<Class>` is a class template specialization: asked once, then remembered. A class mentioned before it is complete - a `virtual_ptr<Cat>` named after `class Cat;`, or `virtual_ptr<Node>` as a member of `Node` - is asked before its base classes, or a declaration further down its body, can be seen. The answer, the default registry, then stuck for the whole translation unit: a method over the class landed in one registry while `BOOST_OPENMETHOD_CLASSES(..., zoo_registry)` registered it in another, and two translation units that completed the class in different orders saw two `virtual_ptr<Cat>` types - `nm` showed `U feed(virtual_ptr<Dog, default_registry>)` in one object against `T feed(virtual_ptr<Dog, zoo_registry>)` in the other. The doc said the opposite: "Being part of the class, a hidden friend cannot be late." Refusing to answer an incomplete class is not open to us: `virtual_ptr<Node> next;` in a plain linked structure declares no affinity, needs none, and has worked since boostorg#109. So every question now carries a tag. The `asked` one builds types, as before; `check_affinity<Class>` puts the same question again under `rechecked` where the class must be complete anyway - a virtual parameter, in `validate_method_parameter`, and a registration, in `use_class_aux` - and refuses an answer that has changed. Declaring nothing stays silent; declaring too late is an error, reported where the wrong registry would do its damage rather than at the mention, where nothing is wrong yet. **The anchor.** `registry_anchor` unwrapped anything with a nested `element_type`, so a polymorphic class that happens to define one - a matrix, a buffer - lost its own declared affinity; an `inplace_vptr` class with one registered into the wrong registry and stored that registry's null `static_vptr` in the object. A smart pointer is now a type `virtual_traits` is specialized for: `detail::virtual_type<T, macro_default_registry>`, no `virtual_type` in the tree depending on the registry. **The return type** was taken verbatim: `-> const zoo_registry` yielded a distinct registry, with its own `registry_state`, disjoint from the one the classes were registered in; `-> int` failed at the first `sizeof(virtual_ptr<Animal>)`, far from the declaration. It is stripped of cv-qualifiers and checked with `is_registry`. **The sentinel.** The catch-all returned `BOOST_OPENMETHOD_DEFAULT_REGISTRY` itself, so "no opinion" had to be recovered by comparing with the macro. An affinity explicitly declared for the default registry was therefore treated as none and yielded in a mixed method, and a registry spelled on a `virtual_ptr` parameter counted as a declared affinity - so a method over `virtual_ptr<Cat, other_registry>` naming no registry, formerly the `registry mismatch` error, silently landed in `other_registry`. The catch-all now returns `detail::default_affinity`. `declared` is the raw answer and constrains a method; `registry_affinity` maps the sentinel to the macro default and stays a query that always answers a registry. A `virtual_ptr` parameter contributes its class's affinity, never the registry it spells. **The mismatch check** covered only a by-value `virtual_ptr`: `virtual_<const Animal&>` and `const virtual_ptr<Animal>&` contradictions compiled clean. All four shapes are checked now. Along the way, a class can declare its affinity with `using boost_openmethod_registry = R;`. Member lookup finds it: inherited, hidden by a derived class's own, ambiguous between two bases that disagree. It takes precedence over the overload, and being visible from the point it is declared, it is the spelling for a class that mentions `virtual_ptr` of itself in its own body - which has already decided to be openmethod-aware, so the typedef intrudes no further. `inplace_vptr_base` provides it. It also sidesteps the drawbacks of a free function found only by ADL: a wrong namespace, a translation unit that does not see it, a function template that outranks it. Nine compile-fail tests, one per diagnosis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS
jll63
added a commit
to jll63/openmethod
that referenced
this pull request
Sep 15, 2026
…ered Review of this branch turned up five ways `registry_affinity` gives a wrong or unchecked answer, each reproduced on gcc 13, clang 18 and MSVC. **Memoization.** `registry_affinity<Class>` is a class template specialization: asked once, then remembered. A class mentioned before it is complete - a `virtual_ptr<Cat>` named after `class Cat;`, or `virtual_ptr<Node>` as a member of `Node` - is asked before its base classes, or a declaration further down its body, can be seen. The answer, the default registry, then stuck for the whole translation unit: a method over the class landed in one registry while `BOOST_OPENMETHOD_CLASSES(..., zoo_registry)` registered it in another, and two translation units that completed the class in different orders saw two `virtual_ptr<Cat>` types - `nm` showed `U feed(virtual_ptr<Dog, default_registry>)` in one object against `T feed(virtual_ptr<Dog, zoo_registry>)` in the other. The doc said the opposite: "Being part of the class, a hidden friend cannot be late." Refusing to answer an incomplete class is not open to us: `virtual_ptr<Node> next;` in a plain linked structure declares no affinity, needs none, and has worked since boostorg#109. So every question now carries a tag. The `asked` one builds types, as before; `check_affinity<Class>` puts the same question again under `rechecked` where the class must be complete anyway - a virtual parameter, in `validate_method_parameter`, and a registration, in `use_class_aux` - and refuses an answer that has changed. Declaring nothing stays silent; declaring too late is an error, reported where the wrong registry would do its damage rather than at the mention, where nothing is wrong yet. **The anchor.** `registry_anchor` unwrapped anything with a nested `element_type`, so a polymorphic class that happens to define one - a matrix, a buffer - lost its own declared affinity; an `inplace_vptr` class with one registered into the wrong registry and stored that registry's null `static_vptr` in the object. A smart pointer is now a type `virtual_traits` is specialized for: `detail::virtual_type<T, macro_default_registry>`, no `virtual_type` in the tree depending on the registry. **The return type** was taken verbatim: `-> const zoo_registry` yielded a distinct registry, with its own `registry_state`, disjoint from the one the classes were registered in; `-> int` failed at the first `sizeof(virtual_ptr<Animal>)`, far from the declaration. It is stripped of cv-qualifiers and checked with `is_registry`. **The sentinel.** The catch-all returned `BOOST_OPENMETHOD_DEFAULT_REGISTRY` itself, so "no opinion" had to be recovered by comparing with the macro. An affinity explicitly declared for the default registry was therefore treated as none and yielded in a mixed method, and a registry spelled on a `virtual_ptr` parameter counted as a declared affinity - so a method over `virtual_ptr<Cat, other_registry>` naming no registry, formerly the `registry mismatch` error, silently landed in `other_registry`. The catch-all now returns `detail::default_affinity`. `declared` is the raw answer and constrains a method; `registry_affinity` maps the sentinel to the macro default and stays a query that always answers a registry. A `virtual_ptr` parameter contributes its class's affinity, never the registry it spells. **The mismatch check** covered only a by-value `virtual_ptr`: `virtual_<const Animal&>` and `const virtual_ptr<Animal>&` contradictions compiled clean. All four shapes are checked now. Along the way, a class can declare its affinity with `using boost_openmethod_registry = R;`. Member lookup finds it: inherited, hidden by a derived class's own, ambiguous between two bases that disagree. It takes precedence over the overload, and being visible from the point it is declared, it is the spelling for a class that mentions `virtual_ptr` of itself in its own body - which has already decided to be openmethod-aware, so the typedef intrudes no further. `inplace_vptr_base` provides it. It also sidesteps the drawbacks of a free function found only by ADL: a wrong namespace, a translation unit that does not see it, a function template that outranks it. Nine compile-fail tests, one per diagnosis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS
jll63
added a commit
that referenced
this pull request
Sep 15, 2026
* virtual parameters obtain their registry by ADL
A class can now name the registry it belongs to, once, next to itself:
class Animal {
friend auto boost_openmethod_registry(Animal*) -> zoo_registry;
};
The class then has an *affinity* for that registry, inherited by its derived
classes, and everything that mentions it finds it: `virtual_ptr`, its deduction
guides, `final_virtual_ptr`, the smart pointer aliases and factories, and any
method that takes the class as a virtual parameter. A method declared without a
registry argument takes the affinity its virtual parameters agree on.
`inplace_vptr.hpp` already had this hook, privately, returning `void` to mean
"no customization". Making the catch-all return BOOST_OPENMETHOD_DEFAULT_REGISTRY
instead lets it serve as a default template argument directly, and makes
backward compatibility structural: with no overload anywhere, every construct
resolves to what it resolved to before. `.text` for test_virtual_ptr_dispatch.cpp
is byte-identical, and the 530 defined symbols are unchanged.
Having *no* affinity is not the same as an affinity for the default registry -
only the former yields. That is what lets a method mix a class that has one with
a class that has none, so a first affinity does not cascade errors through a
codebase. Two conflicting affinities are diagnosed, as is a registry named on a
method that contradicts one of its parameters.
Deliberately out of scope, and documented as such: `virtual_` keeps its single
template parameter; `use_classes` still registers into the macro default unless
a registry is listed last; the `any` and `type_erasure` interop headers are
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
* test: force the instantiation the registry-mismatch check needs
compile_fail_adl_registry_declared_mismatch only declared the method. The
"registry mismatch" static_assert is a fold in `method`'s class body, so it
fires when the class is instantiated - and declaring the method is not enough.
gcc and clang instantiate it anyway through the static registrar; MSVC does
not, so the file compiled and the compile-fail test failed on every Windows
job.
Call the method in main(), the way
compile_fail_virtual_ptr_different_registries.cpp already does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
* rename default_registry_of to registry_affinity
"Affinity" is the term the documentation uses for the relation, so the query
that reads it back should carry it too. `default_registry_of` also read badly
where it mattered most: "Registry defaults to the default registry of Class".
`affine_registry` was the other candidate and is worse - it predicates "affine"
of the registry, when it is the class that has the affinity, and "affine" reads
as affine geometry in a library whose flagship example dispatches on matrix
types.
The concept is adjusted to match the name. Every class now *has* a registry
affinity: a declared one if it declares `boost_openmethod_registry`, the default
affinity otherwise. A declared affinity wins over a default one, which is the
same rule as before - a method may mix a class that declares an affinity with
one that does not - but stated without the awkward "no affinity, which is not
the same as an affinity for the default registry". It also removes a corner that
framing had: a class declared explicitly to the default registry is no longer a
special case, it simply has the default affinity like any other.
detail::affinity_of becomes declared_affinity, and no_affinity becomes
default_affinity, so the internals read the same way as the prose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
* chore: conform to clang-format 22
develop adopted clang-format 22 in 6b02978; reflow the files this branch
touches to match, so the diff carries no formatting noise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoMTqq3duJAptRAbCXgNh1
* registry affinity: a member typedef, and five fixes to how it is answered
Review of this branch turned up five ways `registry_affinity` gives a wrong or
unchecked answer, each reproduced on gcc 13, clang 18 and MSVC.
**Memoization.** `registry_affinity<Class>` is a class template specialization:
asked once, then remembered. A class mentioned before it is complete - a
`virtual_ptr<Cat>` named after `class Cat;`, or `virtual_ptr<Node>` as a member
of `Node` - is asked before its base classes, or a declaration further down its
body, can be seen. The answer, the default registry, then stuck for the whole
translation unit: a method over the class landed in one registry while
`BOOST_OPENMETHOD_CLASSES(..., zoo_registry)` registered it in another, and two
translation units that completed the class in different orders saw two
`virtual_ptr<Cat>` types - `nm` showed `U feed(virtual_ptr<Dog,
default_registry>)` in one object against `T feed(virtual_ptr<Dog,
zoo_registry>)` in the other. The doc said the opposite: "Being part of the
class, a hidden friend cannot be late."
Refusing to answer an incomplete class is not open to us: `virtual_ptr<Node>
next;` in a plain linked structure declares no affinity, needs none, and has
worked since #109. So every question now carries a tag. The `asked` one builds
types, as before; `check_affinity<Class>` puts the same question again under
`rechecked` where the class must be complete anyway - a virtual parameter, in
`validate_method_parameter`, and a registration, in `use_class_aux` - and
refuses an answer that has changed. Declaring nothing stays silent; declaring
too late is an error, reported where the wrong registry would do its damage
rather than at the mention, where nothing is wrong yet.
**The anchor.** `registry_anchor` unwrapped anything with a nested
`element_type`, so a polymorphic class that happens to define one - a matrix, a
buffer - lost its own declared affinity; an `inplace_vptr` class with one
registered into the wrong registry and stored that registry's null
`static_vptr` in the object. A smart pointer is now a type `virtual_traits` is
specialized for: `detail::virtual_type<T, macro_default_registry>`, no
`virtual_type` in the tree depending on the registry.
**The return type** was taken verbatim: `-> const zoo_registry` yielded a
distinct registry, with its own `registry_state`, disjoint from the one the
classes were registered in; `-> int` failed at the first
`sizeof(virtual_ptr<Animal>)`, far from the declaration. It is stripped of
cv-qualifiers and checked with `is_registry`.
**The sentinel.** The catch-all returned `BOOST_OPENMETHOD_DEFAULT_REGISTRY`
itself, so "no opinion" had to be recovered by comparing with the macro. An
affinity explicitly declared for the default registry was therefore treated as
none and yielded in a mixed method, and a registry spelled on a `virtual_ptr`
parameter counted as a declared affinity - so a method over `virtual_ptr<Cat,
other_registry>` naming no registry, formerly the `registry mismatch` error,
silently landed in `other_registry`. The catch-all now returns
`detail::default_affinity`. `declared` is the raw answer and constrains a
method; `registry_affinity` maps the sentinel to the macro default and stays a
query that always answers a registry. A `virtual_ptr` parameter contributes its
class's affinity, never the registry it spells.
**The mismatch check** covered only a by-value `virtual_ptr`: `virtual_<const
Animal&>` and `const virtual_ptr<Animal>&` contradictions compiled clean. All
four shapes are checked now.
Along the way, a class can declare its affinity with `using
boost_openmethod_registry = R;`. Member lookup finds it: inherited, hidden by a
derived class's own, ambiguous between two bases that disagree. It takes
precedence over the overload, and being visible from the point it is declared,
it is the spelling for a class that mentions `virtual_ptr` of itself in its own
body - which has already decided to be openmethod-aware, so the typedef
intrudes no further. `inplace_vptr_base` provides it. It also sidesteps the
drawbacks of a free function found only by ADL: a wrong namespace, a
translation unit that does not see it, a function template that outranks it.
Nine compile-fail tests, one per diagnosis.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
failed to compile with gcc 16:
invalid use of incomplete type 'struct Node', fromstd::is_polymorphic_v<Node>.Three member templates of
virtual_ptr— the constructor fromOther*, and the assignments fromOther&andOther*— constrained themselves withIsPolymorphic<Class, Registry> && std::is_constructible_v<Class*, Other*>in a default templateargument. The first operand does not depend on
Other, so gcc evaluates it when the class itselfis instantiated — for the member declaration, where
Nodeis still incomplete. libstdc++ 13answered
is_polymorphicon an incomplete type; 16 rejects it, as the standard allows.The check now names
Other, through an ignored trailing pack on the exposition-onlyIsPolymorphic, which makes it dependent and defers it to the first use of the member — where theclass is complete. And it sits in a defaulted parameter of its own, after the pointer-convertibility
test: clang reaches these candidates during overload resolution for the member's implicit copy
assignment, with
Other=const virtual_ptr<Node>, and substitution stops at the first conditionthat fails. That is the shape CLAUDE.md prescribes for MrDocs anyway, and the rendered constraint
reads the same.
test/test_virtual_ptr_self_referential.cppcovers the case. Full suite: 165/165 pass locally.🤖 Generated with Claude Code
https://claude.ai/code/session_01JQa4fuiwcfsheZYTCyfPPr