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?
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_ptr
orweak_ptr
instances 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_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 toT*
.Effects: If
r
is empty, constructs an emptyweak_ptr
object; otherwise, constructs aweak_ptr
object that shares ownership withr
and 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_ptr
s, 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.