Skip to content

virtual_ptr: allow a virtual_ptr to the class as a member of the class - #109

Merged
jll63 merged 1 commit into
boostorg:developfrom
jll63:fix/virtual-ptr-incomplete-class
Sep 13, 2026
Merged

jll63 merged 1 commit into
boostorg:developfrom
jll63:fix/virtual-ptr-incomplete-class

Conversation

@jll63

@jll63 jll63 commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator
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.

test/test_virtual_ptr_self_referential.cpp covers the case. Full suite: 165/165 pass locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQa4fuiwcfsheZYTCyfPPr

    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
@cppalliance-bot

Copy link
Copy Markdown

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

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.51%. Comparing base (35f4e39) to head (d44aa59).
⚠️ Report is 11 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             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              
Files with missing lines Coverage Δ
include/boost/openmethod/core.hpp 93.09% <ø> (ø)

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update eb2d5d8...d44aa59. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jll63
jll63 merged commit 10bdfe5 into boostorg:develop Sep 13, 2026
55 of 56 checks passed
@jll63
jll63 deleted the fix/virtual-ptr-incomplete-class branch September 13, 2026 19:48
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants