There have been a number of questions today regarding std::weak_ptr and std::owner_less and their use in the associative containers std::set and std::map. There are a number of posts stating that using a weak_ptr in a std::set is incorrect, since if the weak pointer expires, it will be Undefined Behavior. Is this correct?
Note that the control block used by std::weak_ptr and std::shared_ptr is thread-safe: different non-atomic std::weak_ptr objects can be accessed using mutable operations, such as operator= or reset , simultaneously by multiple threads, even when these instances are copies or otherwise share the same control block ...
std::weak_ptr is a smart pointer that holds a non-owning ("weak") reference to an object that is managed by std::shared_ptr. It must be converted to std::shared_ptr in order to access the referenced object.
By using a weak_ptr , you can create a shared_ptr that joins to an existing set of related instances, but only if the underlying memory resource is still valid. A weak_ptr itself does not participate in the reference counting, and therefore, it cannot prevent the reference count from going to zero.
struct owner_less<void>; (4) (since C++17) This function object provides owner-based (as opposed to value-based) mixed-type ordering of both std::weak_ptr and std::shared_ptr.
One of the reasons std::owner_less exists is to provide this ordering, and guarantee its safety in the presence of expiring weak pointers. My logic is
First, the definition of std::owner_less
operator() defines a strict weak ordering as defined in 25.4
under the equivalence relation defined by
operator(),!operator()(a, b) && !operator()(b, a), twoshared_ptrorweak_ptrinstances are equivalent if and only if they share ownership or are both empty.
The two cases are
Now, I believe the confusion is over the second term. The key is that "empty" in the standard means that the weak_ptr does not share ownership with any object. Again, the standard states
constexpr weak_ptr() noexcept;Effects: Constructs an empty
weak_ptrobject.
Postconditions:use_count() == 0.weak_ptr(const weak_ptr& r) noexcept;template<class Y> weak_ptr(const weak_ptr<Y>& r) noexcept;
template<class Y> weak_ptr(const shared_ptr<Y>& r) noexcept;Requires: The second and third constructors shall not participate in the overload resolution unless
Y*is implicitly convertible toT*.Effects: If
ris empty, constructs an emptyweak_ptrobject; otherwise, constructs aweak_ptrobject that shares ownership withrand stores a copy of the pointer stored inr.Postconditions:
use_count() == r.use_count().
Swap is defined as swapping the states of the two weak_ptrs, and assignment is defined as using the constructors above along with a swap.
They key to note here is that the only way to create an empty weak_ptr is to default construct it, or copy/move/assign one from a previously empty weak_ptr or shared_ptr. It's also important to note that you cannot get an empty weak_ptr by simply letting the weak_ptr expire. An expired weak_ptr simply has a use_count of zero.
As a practical matter, when a shared_ptr is created, a reference count object has to be created, either separate from the data using the shared_ptr constructor, or in the same memory allocation when std::make_shared is used. When a weak_ptr is constructed from that shared_ptr, it will point to that same control structure and reference count. When the shared_ptr is destroyed, it may destroy the data, but the reference count object has to remain until all of the weak_ptr that share ownership are removed. Otherwise, the weak_ptr would have a dangling pointer reference.
So, all of this taken together means that it is safe to use std::weak_ptr as they key of a std::map or in a std::set, as long as you use std::owner_less to perform the ordering. The above guarantees that the ordering of the weak_ptr will remain the same even if it expires while it's in the container.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With