c++multithreadingatomiccompare-and-swapweak-ptr

Is std::atomic<std::weak_ptr<>>::compare_exchange_* guaranteed to work if the underlying pointer is expired?


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?


Solution

  • 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 to expected, assigns desired to p and has synchronization semantics corresponding to the value of success, otherwise assigns p to expected and has synchronization semantics corresponding to the value of failure.

    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?