diff --git a/CLAUDE.md b/CLAUDE.md index 6013f039..73aa6b18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,7 +92,9 @@ the license header: `MATCHES "//[ \t]*expected-error:[ \t]*([^\r\n]+)"`, and hands it to `openmethod_compile_fail_test` as the test's `PASS_REGULAR_EXPRESSION`. Adding a test is dropping in a file - no build-file edit. A file with no marker is a configure-time `FATAL_ERROR`, so a -silently unchecked test cannot slip through. The glob has no `CONFIGURE_DEPENDS` (matching the +silently unchecked test cannot slip through. So is a marker containing a `;`: +`PASS_REGULAR_EXPRESSION` is a CMake list, so the `;` would split the regex into two alternatives +and the test would pass on either half. Write `.*` in its place. The glob has no `CONFIGURE_DEPENDS` (matching the `test_*.cpp` glob above it), so a new file needs a manual re-run of `cmake`. Where the expected wording differs across compilers, match the common substring and say why in a diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index 3003bb6b..0d47532b 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -20,8 +20,8 @@ convenient macros. * xref:#initialize[``] to initialize the library. Typically only included in the translation unit containing `main`. -The following headers make it possible to use standard smart pointers in virtual -parameters: +The following headers make it possible to use standard smart pointers with +`virtual_ptr`: * xref:#std_shared_ptr[``] to use `std::shared_ptr` in virtual parameters. @@ -29,6 +29,9 @@ parameters: * xref:#std_unique_ptr[``] to use `std::unique_ptr` in virtual parameters. +* xref:#std_weak_ptr[``] to track +objects with `std::weak_ptr` without losing their v-table pointer. + ## High-level Headers [#core] @@ -72,6 +75,14 @@ Provides a `virtual_traits` specialization that makes it possible to use a Provides a `virtual_traits` specialization that makes it possible to use a `std::unique_ptr` in place of a raw pointer or reference in virtual parameters. +[#std_weak_ptr] +### link:{headers-url}/boost/openmethod/interop/std_weak_ptr.hpp[] + +Provides cpp:weak_virtual_ptr[], a class that tracks an object with a +`std::weak_ptr` and remembers its v-table pointer. It is not a `virtual_ptr`, +and cannot be used in virtual parameters; its `lock` function returns a +cpp:shared_virtual_ptr[], without a hash table lookup. + [#boost_intrusive_ptr] ### link:{headers-url}/boost/openmethod/interop/boost_intrusive_ptr.hpp[] diff --git a/doc/modules/ROOT/pages/smart_pointers.adoc b/doc/modules/ROOT/pages/smart_pointers.adoc index bd19e994..cfd17665 100644 --- a/doc/modules/ROOT/pages/smart_pointers.adoc +++ b/doc/modules/ROOT/pages/smart_pointers.adoc @@ -74,3 +74,50 @@ pointers: ---- include::example$ast_unique_ptr.cpp[tag=content] ---- + +[#weak_pointers] +## Weak Pointers + +A `std::weak_ptr` observes an object without keeping it alive. Since the object +may be gone, there is nothing to dispatch on: a weak pointer cannot be used in a +virtual parameter, and neither can a `virtual_ptr` to a weak pointer. Still, an +object that is tracked by weak pointers - in a cache, an observer list, or a +back pointer - is typically an object that methods will be called on, once a +weak pointer has been locked. + +- cpp:weak_virtual_ptr[] tracks an object with a `std::weak_ptr`, and + remembers its v-table pointer + +A `weak_virtual_ptr` is a storage facility, not a `virtual_ptr`. It is +constructed from a `shared_virtual_ptr` (or from a `std::shared_ptr` or a +`std::weak_ptr`), and it remembers the v-table pointer along with the weak +pointer. It cannot be dereferenced. Its `lock` function returns a +`shared_virtual_ptr`, which can be passed to methods. Since the v-table pointer +is copied, not looked up, `lock` costs no more than `std::weak_ptr::lock`. It +returns an empty `shared_virtual_ptr` if the object no longer exists. + +[source,c++] +---- +shared_virtual_ptr animal = make_shared_virtual(); +weak_virtual_ptr observer = animal; + +std::cout << poke(observer.lock()) << "\n"; // bark + +animal = nullptr; +std::cout << std::boolalpha << observer.expired() << "\n"; // true +---- + +Remembering the v-table pointer is safe with respect to the lifetime of the +object, because a `std::weak_ptr` keeps the control block alive: once the +object has been destroyed, the weak pointer stays expired, and the v-table +pointer can never be applied to another object. As for any `virtual_ptr`, the +v-table pointer is invalidated if `initialize` is called again, unless the +registry uses the cpp:indirect_vptr[] policy. + +A `weak_virtual_ptr` converts to a `weak_virtual_ptr` to a base class, but not +to a plain or a shared `virtual_ptr`. A cast to a derived class requires the +object: use `lock`, then `cast`. Since it is not a `virtual_ptr`, a +`weak_virtual_ptr` cannot be used in a virtual parameter, but it can be passed +to a method as an ordinary parameter. Support for `std::weak_ptr` is provided +in ``, which also includes the +`std::shared_ptr` header. diff --git a/doc/modules/ROOT/snippets/smart_pointers.cpp b/doc/modules/ROOT/snippets/smart_pointers.cpp index 264a8e41..26291743 100644 --- a/doc/modules/ROOT/snippets/smart_pointers.cpp +++ b/doc/modules/ROOT/snippets/smart_pointers.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #define BOOST_TEST_MODULE openmethod #include @@ -194,3 +195,35 @@ BOOST_AUTO_TEST_CASE(unique_ptr_examples) { BOOST_TEST(cout.str() == "bark\nhiss\n"); } } + +BOOST_AUTO_TEST_CASE(weak_ptr_examples) { + initialize(); + + { + using namespace shared_vptr; + capture_cout cout; + + // tag::weak_lock[] + shared_virtual_ptr animal = make_shared_virtual(); + weak_virtual_ptr observer = animal; + + std::cout << poke(observer.lock()) << "\n"; // bark + + animal = nullptr; + std::cout << std::boolalpha << observer.expired() << "\n"; // true + // end::weak_lock[] + + BOOST_TEST(cout.str() == "bark\ntrue\n"); + } + + { + // tag::weak_pointer[] + shared_virtual_ptr animal = make_shared_virtual(); + weak_virtual_ptr observer = animal; + std::weak_ptr weak = observer.pointer(); + + BOOST_TEST(animal.pointer().use_count() == 1); + BOOST_TEST(weak.lock() == animal.pointer()); + // end::weak_pointer[] + } +} diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index b7d5920d..734eaf8f 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -137,6 +137,9 @@ using macro_default_registry = BOOST_OPENMETHOD_DEFAULT_REGISTRY; template constexpr bool false_t = false; // workaround before CWG2518/P2593R1 +template +struct virtual_ptr_access; + } // namespace detail namespace detail { @@ -955,6 +958,26 @@ inline auto unbox_vptr(const vptr_type* vpp) { inline vptr_type null_vptr = nullptr; +// Access to the parts of a `virtual_ptr`, for the classes that carry a +// v-table pointer of their own and exchange it with one: copy it from a +// `virtual_ptr`, hand it back later. The pointer is the boxed one - under +// `indirect_vptr`, the address of the cell that `initialize()` rewrites, which +// the public `vptr()` unboxes away - and constructing with a given v-table +// pointer skips the lookup, which no public constructor does. +template +struct virtual_ptr_access { + using boxed_vptr_type = decltype(VirtualPtr::vp); + + static auto boxed_vptr(const VirtualPtr& ptr) -> boxed_vptr_type { + return ptr.vp; + } + + template + static auto make(Arg&& obj, boxed_vptr_type vp) -> VirtualPtr { + return VirtualPtr(std::forward(obj), vp); + } +}; + } // namespace detail //! Create a `virtual_ptr` for an object of a known exact class. @@ -1100,6 +1123,8 @@ class virtual_ptr { #ifndef __MRDOCS__ template friend class virtual_ptr; + template + friend struct detail::virtual_ptr_access; template friend auto final_virtual_ptr(Arg&& obj); #endif @@ -1458,6 +1483,8 @@ class virtual_ptr< #ifndef __MRDOCS__ template friend class virtual_ptr; + template + friend struct detail::virtual_ptr_access; template friend auto final_virtual_ptr(Arg&& obj); #endif diff --git a/include/boost/openmethod/interop/std_weak_ptr.hpp b/include/boost/openmethod/interop/std_weak_ptr.hpp new file mode 100644 index 00000000..c3b456df --- /dev/null +++ b/include/boost/openmethod/interop/std_weak_ptr.hpp @@ -0,0 +1,557 @@ +// 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_INTEROP_WEAK_PTR_HPP +#define BOOST_OPENMETHOD_INTEROP_WEAK_PTR_HPP + +#include +#include +#include + +namespace boost::openmethod { + +template> +class weak_virtual_ptr; + +namespace detail { + +// A weak pointer may refer to an object that no longer exists, so there is +// nothing to dispatch on. `virtual_traits` is deliberately *not* specialized +// for `std::weak_ptr`, and `weak_virtual_ptr` is not a `virtual_ptr`. The +// specializations below only replace the vague diagnostics that would result +// from using either as a virtual parameter with a useful one, in the four +// forms a virtual parameter can take. A `weak_virtual_ptr` that is not wrapped +// in `virtual_` is an ordinary parameter, and needs no specialization. + +template +struct reject_weak_parameter : std::false_type { + static_assert( + false_t, + "a weak pointer cannot be a virtual parameter; call lock() first"); +}; + +template +struct validate_method_parameter>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter&>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter&&>, Registry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_>, MethodRegistry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&>, MethodRegistry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&>, MethodRegistry, void> : + reject_weak_parameter {}; + +template +struct validate_method_parameter< + virtual_&&>, MethodRegistry, void> : + reject_weak_parameter {}; + +} // namespace detail + +//! Weak pointer to an object, remembering its v-table pointer +//! +//! A `weak_virtual_ptr` tracks an object with a `std::weak_ptr`, and +//! remembers its v-table pointer. It is a storage facility, not a +//! `virtual_ptr`: it cannot be dereferenced, compared, or used as a virtual +//! parameter, because the object may no longer exist. It can be passed to a +//! method as an ordinary parameter. Call `lock()` to obtain a +//! @ref shared_virtual_ptr, then use it as usual. Since the v-table pointer is +//! copied from the weak pointer, `lock()` costs no more than +//! `std::weak_ptr::lock()`: no hash table lookup is needed. +//! +//! Remembering the v-table pointer is safe with respect to the lifetime of the +//! object: a `std::weak_ptr` keeps the control block alive, so once the object +//! is destroyed, the weak pointer stays expired, and the v-table pointer can +//! never be applied to another object. +//! +//! @note As for any `virtual_ptr`, the remembered v-table pointer is +//! invalidated when @ref boost::openmethod::initialize is called again, unless +//! the registry uses @ref policies::indirect_vptr. +//! +//! @par Example +//! include:smart_pointers.cpp#classes;weak_lock +//! +//! @tparam Class The class of the object, possibly cv-qualified +//! @tparam Registry The registry in which `Class` is registered. Defaults to +//! the registry `Class` has an affinity for, see @ref registry_affinity. +//! +//! @see [Smart Pointers](xref:ROOT:smart_pointers.adoc) +template +class weak_virtual_ptr { +#ifndef __MRDOCS__ + template + friend class weak_virtual_ptr; +#endif + + static constexpr bool use_indirect_vptrs = Registry::has_indirect_vptr; + + std::conditional_t vp; + std::weak_ptr obj; + + template + static auto vptr_of(const std::shared_ptr& other) { + return detail::box_vptr( + other ? detail::acquire_vptr(*other) : detail::null_vptr); + } + + template + static auto vptr_of( + const virtual_ptr, Registry>& other) { + return detail::virtual_ptr_access< + virtual_ptr, Registry>>::boxed_vptr(other); + } + + // Lock `other` once: it is needed to find the dynamic type of the object, + // and the `std::weak_ptr` is then constructed from the `std::shared_ptr`, + // which does not lock again, as construction from a `std::weak_ptr` to a + // different class would. An expired `other` is copied as is, which keeps + // its control block - and with it `expired()`, `use_count()` and owner + // identity - 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 an expired + // source of a *different* class yields an empty weak pointer there. + template + void assign(const std::weak_ptr& other) { + auto locked = other.lock(); + vp = vptr_of(locked); + + if (locked) { + obj = locked; + } else { + obj = other; + } + } + + public: + //! Class pointed to by the `std::weak_ptr` + using element_type = Class; + + //! Default constructor + //! + //! Construct an empty `std::weak_ptr`. Set the v-table pointer to + //! `nullptr`. + weak_virtual_ptr() : + vp(detail::box_vptr(detail::null_vptr)) { + } + + //! Construct from `nullptr` + //! + //! Construct an empty `std::weak_ptr`. Set the v-table pointer to + //! `nullptr`. + //! + //! @param value A `nullptr`. + explicit weak_virtual_ptr(std::nullptr_t) : + vp(detail::box_vptr(detail::null_vptr)) { + } + + weak_virtual_ptr(const weak_virtual_ptr& other) = default; + + weak_virtual_ptr(weak_virtual_ptr&& other) noexcept : + vp(std::exchange( + other.vp, detail::box_vptr(detail::null_vptr))), + obj(std::move(other.obj)) { + } + + //! Construct from a `shared_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`. Construct the `std::weak_ptr` + //! from the `std::shared_ptr` held by `other`. + //! + //! `Other` is _not_ required to be a polymorphic class: the v-table + //! pointer is already known. + //! + //! @par Example + //! include:smart_pointers.cpp#classes;weak_lock + //! + //! @param other A `shared_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be constructible from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t, const std::shared_ptr&>>> + weak_virtual_ptr( + const virtual_ptr, Registry>& other) : + vp(vptr_of(other)), obj(other.pointer()) { + } + + //! Construct from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer and the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be constructible from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t, const std::weak_ptr&>>> + weak_virtual_ptr(const weak_virtual_ptr& other) : + vp(other.vp), obj(other.obj) { + } + + //! Move-construct from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`, and set it to `nullptr` in + //! `other`. Move the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be constructible from + //! `std::weak_ptr&&`. + template< + class Other, + typename = std::enable_if_t, std::weak_ptr&&>>> + weak_virtual_ptr(weak_virtual_ptr&& other) noexcept : + vp(std::exchange( + other.vp, detail::box_vptr(detail::null_vptr))), + obj(std::move(other.obj)) { + } + + //! Construct from a `std::shared_ptr` to a derived class + //! + //! Construct the `std::weak_ptr` from `other`. Set the v-table pointer + //! according to the dynamic type of `*other`. + //! + //! @param other A `std::shared_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be constructible from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t, const std::shared_ptr&>>> + weak_virtual_ptr(const std::shared_ptr& other) : + vp(vptr_of(other)), obj(other) { + } + + //! Construct from a `std::weak_ptr` to a derived class + //! + //! Construct the `std::weak_ptr` from `other`. Lock `other` to find the + //! dynamic type of the object, and set the v-table pointer accordingly. If + //! `other` has expired, the v-table pointer is set to `nullptr`. + //! + //! @param other A `std::weak_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be constructible from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t, const std::weak_ptr&>>> + weak_virtual_ptr(const std::weak_ptr& other) { + assign(other); + } + + //! Assign from `nullptr` + //! + //! Reset the `std::weak_ptr`. Set the v-table pointer to `nullptr`. + //! + //! @param value A `nullptr`. + weak_virtual_ptr& operator=(std::nullptr_t) noexcept { + reset(); + return *this; + } + + weak_virtual_ptr& operator=(const weak_virtual_ptr& other) = default; + + weak_virtual_ptr& operator=(weak_virtual_ptr&& other) noexcept { + vp = std::exchange( + other.vp, detail::box_vptr(detail::null_vptr)); + obj = std::move(other.obj); + return *this; + } + + //! Assign from a `shared_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`. Assign the `std::weak_ptr` from + //! the `std::shared_ptr` held by `other`. + //! + //! `Other` is _not_ required to be a polymorphic class: the v-table + //! pointer is already known. + //! + //! @param other A `shared_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be assignable from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t&, const std::shared_ptr&>>> + weak_virtual_ptr& operator=( + const virtual_ptr, Registry>& other) { + vp = vptr_of(other); + obj = other.pointer(); + return *this; + } + + //! Assign from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer and the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be assignable from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t&, const std::weak_ptr&>>> + weak_virtual_ptr& operator=( + const weak_virtual_ptr& other) { + vp = other.vp; + obj = other.obj; + return *this; + } + + //! Move-assign from a `weak_virtual_ptr` to a derived class + //! + //! Copy the v-table pointer from `other`, and set it to `nullptr` in + //! `other`. Move the `std::weak_ptr` from `other`. + //! + //! @param other A `weak_virtual_ptr` to an object of a class derived from + //! `Class`. + //! + //! @par Requirements + //! @li `std::weak_ptr` must be assignable from + //! `std::weak_ptr&&`. + template< + class Other, + typename = std::enable_if_t&, std::weak_ptr&&>>> + weak_virtual_ptr& operator=( + weak_virtual_ptr&& other) noexcept { + vp = std::exchange( + other.vp, detail::box_vptr(detail::null_vptr)); + obj = std::move(other.obj); + return *this; + } + + //! Assign from a `std::shared_ptr` to a derived class + //! + //! Assign the `std::weak_ptr` from `other`. Set the v-table pointer + //! according to the dynamic type of `*other`. + //! + //! @param other A `std::shared_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be assignable from + //! `const std::shared_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t&, const std::shared_ptr&>>> + weak_virtual_ptr& operator=(const std::shared_ptr& other) { + vp = vptr_of(other); + obj = other; + return *this; + } + + //! Assign from a `std::weak_ptr` to a derived class + //! + //! Assign the `std::weak_ptr` from `other`. Lock `other` to find the + //! dynamic type of the object, and set the v-table pointer accordingly. If + //! `other` has expired, the v-table pointer is set to `nullptr`. + //! + //! @param other A `std::weak_ptr` to a polymorphic object. + //! + //! @par Requirements + //! @li `Other` must be a polymorphic class, according to the `rtti` + //! policy of `Registry`. + //! @li `std::weak_ptr` must be assignable from + //! `const std::weak_ptr&`. + template< + class Other, + typename = std::enable_if_t>, + typename = std::enable_if_t&, const std::weak_ptr&>>> + weak_virtual_ptr& operator=(const std::weak_ptr& other) { + assign(other); + return *this; + } + + //! Lock the weak pointer + //! + //! Return a `shared_virtual_ptr` to the object, using the remembered + //! v-table pointer. No hash table lookup is performed. + //! + //! @par Example + //! include:smart_pointers.cpp#classes;weak_lock + //! + //! @return A `shared_virtual_ptr` to the object if it still exists, or an + //! empty `shared_virtual_ptr` with a `nullptr` v-table pointer otherwise. + auto lock() const -> virtual_ptr, Registry> { + using shared = virtual_ptr, Registry>; + + if (auto locked = obj.lock()) { + return detail::virtual_ptr_access::make( + std::move(locked), vp); + } + + return shared(); + } + + //! Check whether the object still exists + //! + //! @return `true` if the `std::weak_ptr` is empty or the object has been + //! destroyed, `false` otherwise. + auto expired() const noexcept -> bool { + return obj.expired(); + } + + //! Get the number of `std::shared_ptr` objects sharing the object + //! + //! @return The result of `std::weak_ptr::use_count`. + auto use_count() const noexcept -> long { + return obj.use_count(); + } + + //! Compare owners with a `weak_virtual_ptr` + //! + //! Provide the owner-based ordering that an associative container keyed on + //! `weak_virtual_ptr` needs. Note that `std::owner_less` accepts + //! `std::shared_ptr` and `std::weak_ptr` alone in some implementations, so + //! the comparator is best written as a function object calling + //! `owner_before`. + //! + //! @param other A `weak_virtual_ptr`. + //! + //! @return The result of `std::weak_ptr::owner_before` applied to the + //! `std::weak_ptr` held by `other`. + template + auto owner_before( + const weak_virtual_ptr& other) const noexcept -> bool { + return obj.owner_before(other.obj); + } + + //! Compare owners with a `shared_virtual_ptr` + //! + //! @param other A `shared_virtual_ptr`. + //! + //! @return The result of `std::weak_ptr::owner_before` applied to the + //! `std::shared_ptr` held by `other`. + template + auto owner_before( + const virtual_ptr, Registry>& other) + const noexcept -> bool { + return obj.owner_before(other.pointer()); + } + + //! Release the reference to the object + //! + //! Reset the `std::weak_ptr`. Set the v-table pointer to `nullptr`. + void reset() noexcept { + obj.reset(); + vp = detail::box_vptr(detail::null_vptr); + } + + //! Swap with another `weak_virtual_ptr` + //! + //! @param other A `weak_virtual_ptr` to the same class. + void swap(weak_virtual_ptr& other) noexcept { + std::swap(vp, other.vp); + obj.swap(other.obj); + } + + //! Get the weak pointer to the object + //! + //! @par Example + //! include:smart_pointers.cpp#classes;weak_pointer + //! + //! @return A const reference to the `std::weak_ptr` + auto pointer() const noexcept -> const std::weak_ptr& { + return obj; + } + + //! Get the v-table pointer + //! + //! @return A pointer to the v-table remembered when the `weak_virtual_ptr` + //! was created or assigned, or `nullptr`. + auto vptr() const { + return detail::unbox_vptr(this->vp); + } +}; + +//! Reject a `virtual_ptr` to a `std::weak_ptr` +//! +//! A `std::weak_ptr` may refer to an object that no longer exists, so a +//! `virtual_ptr` cannot track an object through one. This specialization +//! rejects the combination at compile time, which also covers +//! @ref final_virtual_ptr, since that instantiates the `virtual_ptr` it +//! returns. Use @ref weak_virtual_ptr instead. +//! +//! The specialization steps aside if `virtual_traits` is specialized for +//! `std::weak_ptr`. +//! +//! @tparam Class The class pointed to by the `std::weak_ptr`. +//! @tparam Registry A @ref registry. +template +class virtual_ptr< + std::weak_ptr, Registry, + std::enable_if_t< + BOOST_OPENMETHOD_UNLESS_MRDOCS(detail::) + IsSmartPtr, Registry> == false>> { + static_assert( + detail::false_t, + "a std::weak_ptr cannot be wrapped in a virtual_ptr; use " + "weak_virtual_ptr"); +}; + +namespace aliases { +using boost::openmethod::weak_virtual_ptr; +} // namespace aliases + +} // namespace boost::openmethod + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 85954411..7ecbcfa6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -192,7 +192,14 @@ foreach(compile_fail_cpp ${compile_fail_cpp_files}) message(FATAL_ERROR "${testname}.cpp has no `// expected-error: ` comment") endif() - openmethod_compile_fail_test(${testname} "${CMAKE_MATCH_1}") + set(fail_regex "${CMAKE_MATCH_1}") + # PASS_REGULAR_EXPRESSION is a list: a `;` would split the regex into + # alternatives, and the test would pass on either half. + if (fail_regex MATCHES ";") + message(FATAL_ERROR + "${testname}.cpp: the expected-error regex contains a `;`; use `.*`") + endif() + openmethod_compile_fail_test(${testname} "${fail_regex}") endforeach() if (TARGET Boost::dll) diff --git a/test/compile_fail_final_virtual_ptr_weak_ptr.cpp b/test/compile_fail_final_virtual_ptr_weak_ptr.cpp new file mode 100644 index 00000000..de996e59 --- /dev/null +++ b/test/compile_fail_final_virtual_ptr_weak_ptr.cpp @@ -0,0 +1,26 @@ +// 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) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// expected-error: cannot be wrapped in a virtual_ptr + +#include +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() { + } +}; + +BOOST_OPENMETHOD_CLASSES(Animal); + +int main() { + auto felix = std::make_shared(); + std::weak_ptr weak = felix; + auto p = final_virtual_ptr(weak); + return 0; +} diff --git a/test/compile_fail_weak_ptr_parameter.cpp b/test/compile_fail_weak_ptr_parameter.cpp new file mode 100644 index 00000000..69c79f9a --- /dev/null +++ b/test/compile_fail_weak_ptr_parameter.cpp @@ -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) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// A `;` would split the regex into two alternatives (see CMakeLists.txt). +// expected-error: a weak pointer cannot be a virtual parameter.*call lock\(\) first + +#include +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() { + } +}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD(poke, (virtual_>), void); + +BOOST_OPENMETHOD_OVERRIDE(poke, (std::weak_ptr), void) { +} + +int main() { + auto felix = std::make_shared(); + poke(std::weak_ptr(felix)); + return 0; +} diff --git a/test/compile_fail_weak_virtual_ptr_parameter.cpp b/test/compile_fail_weak_virtual_ptr_parameter.cpp new file mode 100644 index 00000000..99cb2d3b --- /dev/null +++ b/test/compile_fail_weak_virtual_ptr_parameter.cpp @@ -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) + +// Expected diagnostic, as a CMake regex (see CMakeLists.txt). +// A `;` would split the regex into two alternatives (see CMakeLists.txt). +// expected-error: a weak pointer cannot be a virtual parameter.*call lock\(\) first + +#include +#include + +using namespace boost::openmethod; + +struct Animal { + virtual ~Animal() { + } +}; +struct Cat : Animal {}; + +BOOST_OPENMETHOD(poke, (virtual_>), void); + +BOOST_OPENMETHOD_OVERRIDE(poke, (weak_virtual_ptr), void) { +} + +int main() { + auto felix = std::make_shared(); + poke(weak_virtual_ptr(felix)); + return 0; +} diff --git a/test/test_adl_registry_smart_ptr.cpp b/test/test_adl_registry_smart_ptr.cpp index 2137433f..b51d4454 100644 --- a/test/test_adl_registry_smart_ptr.cpp +++ b/test/test_adl_registry_smart_ptr.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -59,6 +60,11 @@ static_assert(std::is_same_v< boost_intrusive_virtual_ptr, virtual_ptr, zoo_registry>>); +// weak_virtual_ptr is not a virtual_ptr, but it defaults its registry the +// same way, so it converts to and from the shared_virtual_ptr of its class +static_assert( + std::is_same_v, weak_virtual_ptr>); + BOOST_OPENMETHOD_CLASSES(Animal, Dog, zoo_registry); BOOST_OPENMETHOD(name, (shared_virtual_ptr), std::string); @@ -76,4 +82,9 @@ BOOST_AUTO_TEST_CASE(factories_need_no_registry_argument) { auto owned = make_unique_virtual(); static_assert(std::is_same_v>); + + weak_virtual_ptr observer = dog; + static_assert( + std::is_same_v>); + BOOST_TEST(name(observer.lock()) == "dog"); } diff --git a/test/test_weak_virtual_ptr.cpp b/test/test_weak_virtual_ptr.cpp new file mode 100644 index 00000000..2ebe7a70 --- /dev/null +++ b/test/test_weak_virtual_ptr.cpp @@ -0,0 +1,441 @@ +// 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) + +#include + +#define BOOST_TEST_MODULE weak_virtual_ptr +#include + +#include "test_virtual_ptr_value_semantics.hpp" + +#include +#include +#include +#include + +// A weak virtual_ptr is not a virtual_ptr at all: neither a smart one in the +// `IsSmartPtr` sense (no `virtual_traits`, no `rebind`) nor a plain one. +static_assert(!IsSmartPtr, default_registry>); +static_assert(!is_virtual_ptr>); + +// Moves are noexcept, so containers relocate by moving, not copying. +static_assert(std::is_nothrow_move_constructible_v>); +static_assert(std::is_nothrow_move_assignable_v>); + +static_assert(std::is_same_v::element_type, Animal>); +static_assert(std::is_same_v< + decltype(std::declval>().lock()), + shared_virtual_ptr>); +static_assert(std::is_same_v< + decltype(std::declval>().pointer()), + const std::weak_ptr&>); + +// Construction is allowed from shared and weak pointers, virtual or not... +static_assert(std::is_constructible_v< + weak_virtual_ptr, shared_virtual_ptr>); +static_assert( + std::is_constructible_v, shared_virtual_ptr>); +static_assert( + std::is_constructible_v, weak_virtual_ptr>); +static_assert( + std::is_constructible_v, std::shared_ptr>); +static_assert( + std::is_constructible_v, std::weak_ptr>); +static_assert(std::is_constructible_v< + weak_virtual_ptr, shared_virtual_ptr>); + +// ...but not from a plain pointer, reference or virtual_ptr, nor from a +// different class or a const object... +static_assert(!std::is_constructible_v, Animal&>); +static_assert(!std::is_constructible_v, Animal*>); +static_assert( + !std::is_constructible_v, virtual_ptr>); +static_assert( + !std::is_constructible_v, shared_virtual_ptr>); +static_assert(!std::is_constructible_v< + weak_virtual_ptr, shared_virtual_ptr>); +static_assert( + !std::is_constructible_v< + weak_virtual_ptr, std::shared_ptr>); +static_assert(!std::is_constructible_v< + weak_virtual_ptr, std::weak_ptr>); + +// ...and a weak virtual_ptr converts to nothing but another weak virtual_ptr. +static_assert( + !std::is_constructible_v, weak_virtual_ptr>); +static_assert( + !std::is_assignable_v&, weak_virtual_ptr>); +static_assert(!std::is_constructible_v< + shared_virtual_ptr, weak_virtual_ptr>); +static_assert(!std::is_assignable_v< + shared_virtual_ptr&, weak_virtual_ptr>); +static_assert(!std::is_constructible_v< + std::shared_ptr, weak_virtual_ptr>); + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_from_shared_virtual_ptr, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + shared_virtual_ptr shared(snoopy); + + weak_virtual_ptr weak(shared); + BOOST_TEST(!weak.expired()); + BOOST_TEST(weak.use_count() == 2); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.pointer().lock() == snoopy); + + { + auto locked = weak.lock(); + static_assert(std::is_same_v< + decltype(locked), shared_virtual_ptr>); + BOOST_TEST(locked.get() == snoopy.get()); + BOOST_TEST(locked.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.use_count() == 3); + } + + BOOST_TEST(weak.use_count() == 2); + + shared = nullptr; + snoopy.reset(); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.use_count() == 0); + + auto locked = weak.lock(); + BOOST_TEST(locked.get() == nullptr); + BOOST_TEST(locked.vptr() == nullptr); + + weak.reset(); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_from_std_pointers, Registry, test_policies) { + init_test(); + + auto felix = std::make_shared(); + + { + weak_virtual_ptr weak(felix); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.lock().get() == felix.get()); + } + + { + std::weak_ptr std_weak = felix; + weak_virtual_ptr weak(std_weak); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.lock().get() == felix.get()); + } + + { + // an expired std::weak_ptr yields an expired weak virtual_ptr + std::weak_ptr std_weak; + { + auto dead = std::make_shared(); + std_weak = dead; + } + + weak_virtual_ptr weak(std_weak); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + BOOST_TEST(weak.lock().get() == nullptr); + BOOST_TEST(weak.use_count() == 0); + } + + { + // an expired source of the same class keeps its control block, so it + // keeps its owner identity. Across a class conversion that is up to + // the standard library: libstdc++ shares ownership, as + // [util.smartptr.weak.const] requires of a source that is expired but + // not empty; libc++ locks first, and an expired source then yields an + // empty weak pointer. + std::weak_ptr std_weak; + { + auto dead = std::make_shared(); + std_weak = dead; + } + + weak_virtual_ptr weak(std_weak); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + BOOST_TEST(!weak.pointer().owner_before(std_weak)); + BOOST_TEST(!std_weak.owner_before(weak.pointer())); + } + + { + // an empty std::shared_ptr yields an empty weak virtual_ptr + weak_virtual_ptr weak{std::shared_ptr()}; + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_default_and_nullptr, Registry, test_policies) { + init_test(); + + { + weak_virtual_ptr weak; + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.use_count() == 0); + BOOST_TEST(weak.vptr() == nullptr); + BOOST_TEST(weak.lock().get() == nullptr); + } + + { + weak_virtual_ptr weak(nullptr); + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + } + + { + auto snoopy = std::make_shared(); + weak_virtual_ptr weak(snoopy); + BOOST_TEST(!weak.expired()); + + weak = nullptr; + BOOST_TEST(weak.expired()); + BOOST_TEST(weak.vptr() == nullptr); + } +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_assign, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + weak_virtual_ptr weak; + + weak = shared_virtual_ptr(snoopy); + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak = shared_virtual_ptr(felix); + BOOST_TEST(weak.lock().get() == felix.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak = snoopy; + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak = std::weak_ptr(felix); + BOOST_TEST(weak.lock().get() == felix.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + + weak_virtual_ptr weak_dog(snoopy); + weak = weak_dog; + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(!weak_dog.expired()); + + weak = *&weak; // self-assignment + BOOST_TEST(weak.lock().get() == snoopy.get()); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_copy_move, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + weak_virtual_ptr weak_dog(snoopy); + + { + weak_virtual_ptr copy(weak_dog); + BOOST_TEST(copy.lock().get() == snoopy.get()); + BOOST_TEST(copy.vptr() == Registry::template static_vptr); + BOOST_TEST(weak_dog.lock().get() == snoopy.get()); + } + + { + // upcast, copying + weak_virtual_ptr base(weak_dog); + BOOST_TEST(base.lock().get() == snoopy.get()); + BOOST_TEST(base.vptr() == Registry::template static_vptr); + BOOST_TEST(weak_dog.lock().get() == snoopy.get()); + } + + { + weak_virtual_ptr source(snoopy); + weak_virtual_ptr moved(std::move(source)); + BOOST_TEST(moved.lock().get() == snoopy.get()); + BOOST_TEST(moved.vptr() == Registry::template static_vptr); + BOOST_TEST(source.expired()); + BOOST_TEST(source.vptr() == nullptr); + } + + { + // upcast, moving + weak_virtual_ptr source(snoopy); + weak_virtual_ptr moved(std::move(source)); + BOOST_TEST(moved.lock().get() == snoopy.get()); + BOOST_TEST(moved.vptr() == Registry::template static_vptr); + BOOST_TEST(source.expired()); + BOOST_TEST(source.vptr() == nullptr); + } + + { + weak_virtual_ptr source(snoopy); + weak_virtual_ptr moved; + moved = std::move(source); + BOOST_TEST(moved.lock().get() == snoopy.get()); + BOOST_TEST(moved.vptr() == Registry::template static_vptr); + BOOST_TEST(source.expired()); + BOOST_TEST(source.vptr() == nullptr); + } + + { + auto felix = std::make_shared(); + weak_virtual_ptr weak_cat(felix); + weak_virtual_ptr weak_animal(weak_dog); + weak_cat.swap(weak_animal); + BOOST_TEST(weak_cat.lock().get() == snoopy.get()); + BOOST_TEST(weak_cat.vptr() == Registry::template static_vptr); + BOOST_TEST(weak_animal.lock().get() == felix.get()); + BOOST_TEST(weak_animal.vptr() == Registry::template static_vptr); + } +} + +// `std::owner_less` is generic only in libstdc++; MSVC's and libc++'s +// have overloads for `std::shared_ptr` and `std::weak_ptr` alone, so a +// container keyed on owner identity carries its own comparator. +struct owner_less { + template + auto operator()(const Left& left, const Right& right) const -> bool { + return left.owner_before(right); + } +}; + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_owner_before, Registry, test_policies) { + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + shared_virtual_ptr shared_dog(snoopy); + weak_virtual_ptr weak_dog(shared_dog); + weak_virtual_ptr weak_dog_too(snoopy); + weak_virtual_ptr weak_cat(felix); + + // same owner, against a shared and a weak virtual_ptr, to another class + BOOST_TEST(!weak_dog.owner_before(shared_dog)); + BOOST_TEST(!weak_dog.owner_before(weak_dog_too)); + BOOST_TEST(!weak_dog_too.owner_before(weak_dog)); + + // different owners: a strict weak ordering, the same as std::weak_ptr's + BOOST_TEST( + weak_dog.owner_before(weak_cat) != weak_cat.owner_before(weak_dog)); + BOOST_TEST( + weak_dog.owner_before(weak_cat) == + std::weak_ptr(snoopy).owner_before(felix)); + + std::set, owner_less> observers; + observers.insert(weak_dog); + observers.insert(weak_cat); + observers.insert(weak_virtual_ptr(snoopy)); // same owner + BOOST_TEST(observers.size() == 2u); + BOOST_TEST(observers.count(weak_dog) == 1u); + BOOST_TEST(observers.count(weak_cat) == 1u); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_non_polymorphic, Registry, test_policies) { + // The v-table pointer is copied from the shared_virtual_ptr, so the class + // need not be polymorphic; only the vptr lookup would require that. + BOOST_OPENMETHOD_REGISTER(use_classes); + init_test(); + + auto shared = make_shared_virtual(); + weak_virtual_ptr weak(shared); + BOOST_TEST(weak.vptr() == Registry::template static_vptr); + BOOST_TEST(weak.lock().get() == shared.get()); + BOOST_TEST( + weak.lock().vptr() == Registry::template static_vptr); +} + +struct BOOST_OPENMETHOD_ID(poke); + +// Namespace-scope templates: gcc before 13 does not accept the static member +// functions of a local class as template arguments (no linkage). +template +auto poke_dog(shared_virtual_ptr) -> std::string { + return "bark"; +} + +template +auto poke_cat(shared_virtual_ptr) -> std::string { + return "hiss"; +} + +struct BOOST_OPENMETHOD_ID(observe); + +// A weak_virtual_ptr is not a virtual_ptr, so it can be an ordinary, non-virtual +// parameter of a method: the object it tracks is not dispatched on. +template +auto observe_dog( + virtual_ptr, weak_virtual_ptr other) + -> std::string { + return other.expired() ? "dog sees nobody" : "dog sees somebody"; +} + +template +auto observe_cat( + virtual_ptr, weak_virtual_ptr other) + -> std::string { + return other.expired() ? "cat sees nobody" : "cat sees somebody"; +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_non_virtual_parameter, Registry, test_policies) { + using observe = method< + BOOST_OPENMETHOD_ID(observe), + auto(virtual_ptr, weak_virtual_ptr) + ->std::string, + Registry>; + + BOOST_OPENMETHOD_REGISTER( + typename observe::template override< + observe_dog, observe_cat>); + + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + virtual_ptr dog(*snoopy); + virtual_ptr cat(*felix); + weak_virtual_ptr weak_dog(snoopy); + weak_virtual_ptr weak_cat(felix); + + BOOST_TEST(observe::fn(dog, weak_cat) == "dog sees somebody"); + BOOST_TEST(observe::fn(cat, weak_dog) == "cat sees somebody"); + + felix.reset(); + BOOST_TEST(observe::fn(dog, weak_cat) == "dog sees nobody"); +} + +BOOST_AUTO_TEST_CASE_TEMPLATE( + weak_virtual_ptr_dispatch, Registry, test_policies) { + using poke = method< + BOOST_OPENMETHOD_ID(poke), + auto(shared_virtual_ptr)->std::string, Registry>; + + BOOST_OPENMETHOD_REGISTER( + typename poke::template override< + poke_dog, poke_cat>); + + init_test(); + + auto snoopy = std::make_shared(); + auto felix = std::make_shared(); + weak_virtual_ptr weak_dog(snoopy); + weak_virtual_ptr weak_cat(felix); + + // lock, then dispatch + BOOST_TEST(poke::fn(weak_dog.lock()) == "bark"); + BOOST_TEST(poke::fn(weak_cat.lock()) == "hiss"); +}