I have a block of code where std::atomic<std::weak_ptr<T>>
doesn't behave the way I would have expected if the underlying weak pointer is expired:
std::atomic<std::weak_ptr<Widget>> ptrAtomicWidget = ...;
std::shared_ptr<Widget> ptrWidget = ptrAtomicWidget.load().lock();
while (ptrWidget == nullptr)
{
ptrWidget = std::make_shared<Widget>();
std::weak_ptr<Widget> ptrExpected; // <--- nullptr
std::weak_ptr<Widget> ptrDesired = ptrWidget;
// Problem Version: Causes an infinite loop when ptrExpected is expired
if (!ptrAtomicWidget.compare_exchange_weak(ptrExpected, ptrDesired))
{
ptrWidget = ptrExpected().lock();
}
// Potential Repair Version: *seems* to work (could alternately move declaration of ptrExpected above while loop)
if (!ptrAtomicWidget.compare_exchange_weak(ptrExpected, ptrDesired)
&& ptrExpected.expired()
&& !ptrAtomicWidget.compare_exchange_weak(ptrExpected, ptrDesired))
{
ptrWidget = ptrExpected().lock();
}
}
The problem I'm having involves the "seems to work" part of the "potential repair version" of the loop body. The repair requires two different expired weak_ptrs to reliably compare equal to each other during the compare_exchange. std::weak_ptr
doesn't have an equality operator so consequently its documentation is mute on this. None of the documentation I can find for the specialization of std::atomic<>
, for example at CPPReference, describes the behavior of compare-exchange when the pointer is expired. I don't know whether it just happens to work with my particular compiler or whether the C++ standard guarantees it. Does someone know if it is guaranteed to work by the standard?
You have misunderstood the condition for when compare_exchange
would succeed for weak pointers.
According to the specification for atomic<weak_ptr<T>>::compare_exchange_weak
:
Effects: If
p
is equivalent toexpected
, assignsdesired
top
and has synchronization semantics corresponding to the value ofsuccess
, otherwise assignsp
toexpected
and has synchronization semantics corresponding to the value offailure
.
It is important when two pointers are equivalent, which is also explained:
Remarks: Two
weak_ptr
objects are equivalent if they store the same pointer value and either share ownership or are both empty. The weak form may fail spuriously. See [atomics.types.operations].
A ptrExpected
initialized to nullptr
(or nothing) does not store the same pointer value as ptrAtomicWidget
(unless it is null), so the first attempt at a compare-exchange is always going to fail.
You can also see why this happens in the libstdc++ implementation (bits/shared_ptr_atomic.h
).
When _M_ptr == __expected._M_ptr
fails (e.g. when one weak_ptr
is null and the other isn't), no compare-exchange is attempted and the result
is always false
.
The second "workaround loop" "works" because when the first compare-exchange inevitably fails, the current value of ptrAtomicWidget
is loaded into ptrExpected
, which makes it at possible for ptrDesired
to replace it upon the second attempt.
To me it seems like && ptrExpected.expired()
can be removed because you only would have entered the loop if ptrWidget == nullptr
, which implies that ptrAtomicWidget
is null or expired.
See also When is a std::weak_ptr empty? Is an expired std::weak_ptr empty?