Skip to content

interop: weak_virtual_ptr for std::weak_ptr, as a class of its own - #110

Open
jll63 wants to merge 3 commits into
boostorg:developfrom
jll63:feature/weak-ptr-standalone
Open

jll63 wants to merge 3 commits into
boostorg:developfrom
jll63:feature/weak-ptr-standalone

Conversation

@jll63

@jll63 jll63 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

(Written by Claude Code, on behalf of @jll63.)

Closes #73. Supersedes #100, whose two commits this branch carries unchanged, plus one more.

Summary

Adds <boost/openmethod/interop/std_weak_ptr.hpp>, providing weak_virtual_ptr<Class>: a class that tracks an object with a std::weak_ptr and remembers its v-table pointer, so that lock() returns a shared_virtual_ptr with no hash table lookup — it costs exactly std::weak_ptr::lock() — and the class need not be polymorphic. Remembering the vptr is safe with respect to the object's lifetime: the weak pointer keeps the control block alive, so once the object is destroyed it stays expired for good. As for any virtual_ptr, the vptr is invalidated by a second initialize() unless the registry uses indirect_vptr.

weak_virtual_ptr is not a virtual_ptr. #100 modelled it as a partial specialization virtual_ptr<std::weak_ptr<Class>>, and a /code-review max pass found that shape to be the root cause of most of its findings: being a virtual_ptr made it match is_virtual (so it could not be an ordinary, non-virtual method parameter), the plain virtual_ptr's converting constructors (hence a has_get gate in core.hpp), the free operator== (which then failed inside core.hpp), final_virtual_ptr, and a user virtual_traits<std::weak_ptr> made the specialization ambiguous. As a class of its own it matches none of that:

  • constructed/assigned from a shared_virtual_ptr (copies the vptr), another weak_virtual_ptr (upcasts, copy or move), or a std::shared_ptr / std::weak_ptr (acquires the vptr; expired ⇒ null vptr, control block kept);
  • lock(), expired(), use_count(), owner_before() (against a weak or a shared virtual_ptr), swap(), reset(), pointer(), vptr(); moves are noexcept. owner_before is what an owner-keyed container's comparator calls; note that std::owner_less<void> is generic only in libstdc++ — MSVC's and libc++'s accept std::shared_ptr and std::weak_ptr alone — so such a container is best given a comparator of its own;
  • deliberately no get(), operator*, operator->, cast<>(), comparison operators or make_weak_virtual;
  • usable as a non-virtual method parameter (observe(virtual_ptr<Animal>, weak_virtual_ptr<Animal>)), not as a virtual one: virtual_<std::weak_ptr<T>> and virtual_<weak_virtual_ptr<T>>, in all four forms (value, &, const&, &&), are rejected with "a weak pointer cannot be a virtual parameter; call lock() first";
  • a virtual_ptr<std::weak_ptr<T>> — which is what final_virtual_ptr(std::weak_ptr) would build — is rejected with "a std::weak_ptr cannot be wrapped in a virtual_ptr; use weak_virtual_ptr", unless virtual_traits is specialized for std::weak_ptr, in which case the specialization steps aside.
shared_virtual_ptr<Animal> animal = make_shared_virtual<Dog>();
weak_virtual_ptr<Animal> observer = animal;

std::cout << poke(observer.lock()) << "\n"; // bark

animal = nullptr;
std::cout << std::boolalpha << observer.expired() << "\n"; // true

The one change in core.hpp

What the weak class needs from a shared_virtual_ptr has no public route: the boxed v-table pointer (under indirect_vptr, the address of the cell that initialize() rewrites, which vptr() unboxes away) and the private constructor that takes a v-table pointer. Rather than have core befriend a class from an interop header, core.hpp gains detail::virtual_ptr_access<VirtualPtr> — a generic door to those two things (boxed_vptr_type, boxed_vptr(ptr), make(obj, vptr)), befriended by both virtual_ptr specializations in the same way registry befriends detail::use_class_aux. std_weak_ptr.hpp is an ordinary client of it; core names no client class, and the coming boost::weak_ptr adaptor uses the same door. final_virtual_ptr is unchanged.

Rebased on develop 28494b6 (#96): like the smart pointer aliases, weak_virtual_ptr defaults its registry to the affinity its class declares, so it agrees with the shared_virtual_ptr it converts to and from. test/test_adl_registry_smart_ptr.cpp covers it.

Tests and docs

  • test/test_weak_virtual_ptr.cpp: 10 cases over the 4 direct/indirect × vector/map registries (including dispatch with a weak non-virtual parameter, an owner-keyed std::set, swap, an expired source), plus static_asserts on the allowed and rejected conversions and on nothrow moves.
  • Three compile-fail tests: virtual_<std::weak_ptr<T>>, virtual_<weak_virtual_ptr<T>>, final_virtual_ptr(std::weak_ptr). Their expected-error markers no longer contain a ;PASS_REGULAR_EXPRESSION is a CMake list, so a ; split each regex into two alternatives and a test passed on either half; test/CMakeLists.txt now refuses such a marker.
  • "Weak Pointers" section in smart_pointers.adoc (after the AST example, describing a class rather than an alias), ref_headers.adoc entries, weak_lock / weak_pointer tags in the smart_pointers.cpp snippet, referenced by the doc comments.

Verified locally: gcc 13.3 Debug -Werror, full suite 189/189 (rebased on develop 28494b6, so including #96's tests); clang 22 on the new test, the compile-fail tests and a set of ad-hoc probes; all compile-fail tests report the intended static_assert as the first error on both compilers; Antora build clean (no MrDocs leakage, weak_virtual_ptr has its own reference page, all @ref/cpp: targets resolve). CI on the fork: https://github.com/jll63/openmethod/actions/runs/34971218600.

🤖 Generated with Claude Code

https://claude.ai/code/session_018FzFtVWNk6d4Wut5SboCZC

jll63 and others added 2 commits September 15, 2026 08:42
Closes boostorg#73.

Add <boost/openmethod/interop/std_weak_ptr.hpp>, providing weak_virtual_ptr<Class>,
an alias for virtual_ptr<std::weak_ptr<Class>>. It tracks an object with a
std::weak_ptr and remembers its v-table pointer, so that lock() returns a
shared_virtual_ptr without a hash table lookup. Remembering the vptr is safe:
the weak pointer keeps the control block alive, so once the object is destroyed
it stays expired for good.

It is a storage facility only. It is constructed from a shared_virtual_ptr (or
a std::shared_ptr or std::weak_ptr), converts to a weak_virtual_ptr to a base
class, and offers lock(), expired(), use_count(), reset(), pointer() and
vptr(). It cannot be dereferenced, and it cannot be used as a virtual
parameter - neither as virtual_<std::weak_ptr<T>> nor as weak_virtual_ptr<T> -
because the object may no longer exist; validate_method_parameter
specializations reject both with "a weak pointer cannot be a virtual parameter;
call lock() first".

std::weak_ptr does not fit the generic smart-pointer specialization of
virtual_ptr, which needs get(), operator* and a conversion to bool, so the
specialization is written by hand. virtual_traits is deliberately not
specialized for std::weak_ptr: that keeps IsSmartPtr false, which is what lets
the hand-written specialization win.

In core.hpp, the plain virtual_ptr's converting constructor and assignment from
another virtual_ptr now also require the source to have get() (detail::has_get),
so a weak source is a clean substitution failure instead of a hard error in the
body, and is_constructible reports it correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… test

The dispatch test registered the static member functions of a local class
as override<> template arguments. That is valid C++17, but gcc before 13
rejects it ("has no linkage"), failing the gcc-10/11/12 and Cygwin 32-bit
jobs. Use namespace-scope function templates instead, as test_util.hpp's
poke_bear does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jll63
jll63 force-pushed the feature/weak-ptr-standalone branch from b485a4b to 5d03e1c Compare September 15, 2026 12:45
@cppalliance-bot

cppalliance-bot commented Sep 15, 2026

Copy link
Copy Markdown

An automated preview of the documentation is available at https://110.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-15 14:02:10 UTC

@jll63
jll63 force-pushed the feature/weak-ptr-standalone branch from 5d03e1c to c1a8a6a Compare September 15, 2026 12:50
As a partial specialization of virtual_ptr, weak_virtual_ptr matched
everything written for virtual_ptr: is_virtual, so it could not be an
ordinary non-virtual method parameter; the converting constructors of
the plain virtual_ptr, hence the has_get gate in core.hpp; the free
operator==, which then failed inside core.hpp; final_virtual_ptr; and a
user virtual_traits<std::weak_ptr> made it ambiguous. A weak pointer is
not a virtual_ptr - it cannot be dereferenced or dispatched on - so
model it as a class of its own, with the same members.

What it needs from a shared virtual_ptr has no public route: the boxed
v-table pointer, which under indirect_vptr is the address of the cell
that initialize() rewrites and which vptr() unboxes away, and the
constructor that takes a v-table pointer, so that lock() skips the
lookup. core.hpp therefore gains detail::virtual_ptr_access, a generic
door to those two things, befriended by both virtual_ptr
specializations; weak_virtual_ptr uses it to copy the pointer from a
shared virtual_ptr and hand it back in lock(). core names no client
class: the next adaptor - boost::weak_ptr - uses the same door.

Fold in the review's findings on the way. Move constructors and
assignments are noexcept, so containers relocate by moving. Construction
from a std::weak_ptr to another class locks it once, not twice, and an
expired source is copied as is, so that it keeps its control block as
far as the standard library allows - libstdc++ shares ownership with a
source that is expired but not empty, as [util.smartptr.weak.const]
requires; libc++ locks first, so there an expired source of a different
class yields an empty weak pointer. owner_before and swap support the
owner-keyed containers a cache of weak pointers is built on - with a
comparator of one's own, since std::owner_less<void> is generic only in
libstdc++; MSVC's and libc++'s accept std::shared_ptr and std::weak_ptr
alone. The "call lock() first" diagnostic now fires for every form of a
weak virtual parameter - value, &, const& and && - and for
virtual_<weak_virtual_ptr<T>>; a bare weak_virtual_ptr<T> parameter is
valid. A virtual_ptr<std::weak_ptr<T>>, which is what
final_virtual_ptr(std::weak_ptr) would build, is rejected with "use
weak_virtual_ptr", unless virtual_traits is specialized for
std::weak_ptr. The doc comment says what a remembered v-table pointer is
safe against, and what it is not: a second initialize(), unless the
registry uses indirect_vptr.

Like the smart pointer aliases, weak_virtual_ptr defaults its registry
to the affinity its class declares, so that it agrees with the
shared_virtual_ptr it converts to and from.

The compile-fail markers no longer contain a `;`: PASS_REGULAR_EXPRESSION
is a CMake list, so it split each regex into two alternatives, and a
test passed on either half. The CMake loop now refuses such a marker.

Docs: the Weak Pointers section no longer swallows the unique_ptr AST
example, describes a class rather than an alias, and ref_headers.adoc no
longer lists the header under "use in virtual parameters".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018FzFtVWNk6d4Wut5SboCZC
@jll63
jll63 force-pushed the feature/weak-ptr-standalone branch from c1a8a6a to ee6e1df Compare September 15, 2026 13:58
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.

support std::weak_ptr

2 participants