One point of clarification. Expired weak_ptr's are not UB when using owner_less. From the standard
under the equivalence relation defined by operator(), !operator()(a, b) && !operator()(b, a), two shared_ptr or weak_ptr instances are equivalent if and only if they share ownership or are both empty.
One thing to remember is that an empty weak_ptr is one that has never been assigned a valid shared_ptr, or one which has been assigned an empty shared_ptr/weak_ptr. A weak_ptr that has expired is not an empty weak_ptr.
Edit:
The definition above hinges on what does it mean to have an "empty" weak_ptr. So, let's look at the standard
constexpr weak_ptr() noexcept;
Effects: Constructs an empty weak_ptr object.
Postconditions: use_count() == 0.- weak_ptr(const weak_ptr& r) noexcept;
- template weak_ptr(const weak_ptr& r) noexcept;
template weak_ptr(const shared_ptr& r) noexcept;
Requires: The second and third constructors shall not participate in the overload resolution unless Y* is implicitly convertible to T*.
Effects: If r is empty, constructs an empty weak_ptr object; otherwise, constructs a weak_ptr object that shares ownership with r and stores a copy of the pointer stored in r.
Postconditions: use_count() == r.use_count().
Swapping simply exchanges contents, and assignment is defined as the above constructors plus a swap.
To create an empty weak_ptr
, you use the default constructor, or pass it a weak_ptr or shared_ptr that is empty. Now, you'll note expiration doesn't actually cause a weak_ptr to become empty. It simply causes it to have a use_count()
of zero and expired()
to return true. This is because the underlying reference count cannot be released until all the weak pointers that shared the object are also released.