Question

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?

Was it helpful?

Solution

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), two shared_ptr or weak_ptr instances are equivalent if and only if they share ownership or are both empty.

The two cases are

  1. They share the same object, which in practical terms means they share the same reference count object.
  2. They are both empty.

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_ptr object.
    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 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().

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top