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_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.
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