3

When you have a weak_ptr, and you do:

std::weak_ptr<int> wp; // Pretend it's assigned to a shared_ptr
if (!wp.expired()) // Equivalent to use_count() != 0
{ /* Here we're not 100% sure the raw pointer is accessible because a shared_ptr from another thread
   could have been destroyed, and is in the process of decrementing the use_count from 1 to 0 and calling the deleter.*/

}

On the other hand:

std::weak_ptr<int> wp; // Pretend it's assigned to a shared_ptr
if (wp.expired()) Equivalent to use_count() == 0
{ /* Here we are sure that the raw pointer is invalid because of the interesting property that once the use_count is 0 it can't get incremented anymore */

}

The correct way to access a pointer if the resource is still valid is by calling lock, which returns a shared_ptr to the resource if it's valid, or an empty/null shared_ptr if the resource is not valid:

if (std::shared_ptr shp = wp.lock())
{// We know the resource is valid, we can use it
}

The thing I don't get is that the lock function is defined, according to the docs, as:

Effectively returns expired() ? shared_ptr<T>() : shared_ptr<T>(*this), executed atomically.

So if expired() (equivalent to use_count() == 0) returns true, then we know for sure that the pointer can't be dereferenced, we get back an empty shared_ptr. On the other hand, if it's expired() returns false we get back a shared_ptr to the resource, but the thing is that, as I mentioned above, when expired() returns true we know the pointer's bad, but when it returns false it can be a false negative, but we get returned a shared_ptr. When we check that returned shared_ptr another thread could still be in the process of writing to the control block, right?

When the docs say "executed atomically" does it mean that calling lock() method uses a mutex every time you want to access the weak_ptr, or does it use an atomic operation? If so, what is the performance cost of this?

I don't think it uses atomic types for the counters because I heard about a trick that's often implemented which I think is a method of avoiding using atomic types/operations:

Adding one for all shared_ptr instances is just an optimization (saves one atomic increment/decrement when copying/assigning shared_ptr instances) Link.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • Honestly, I don't see what the benefit of `weak_ptr::expired()` is. In a multi-threaded environment, where the final strong reference via `shared_ptr` could be released on another thread, If `expired()` returns true, you know definitively that the object is deleted, but if it returns `false` the "aliveness" of the object isn't valid for any longer than that. You can't safely assume anything until you call `lock()` – selbie May 06 '21 at 05:44
  • As per docs on [cppreference.com](https://en.cppreference.com/w/cpp/memory/weak_ptr/expired): `This function is inherently racy if the managed object is shared among threads. In particular, a false result may become stale before it can be used. A true result is reliable`. – selbie May 06 '21 at 05:46
  • Final comment. I think `expired` is a hold over from Boost where itt explicitly suggests the underlying `use_count` function is "Use only for debugging and testing purposes, not for production code.". – selbie May 06 '21 at 05:49
  • @selbie So every time you want to access the weak_ptr resource you must use an atomic operation? Is that's what's happening with lock()? Or mutex? I don't even know the difference. – Zebrafish May 06 '21 at 05:59
  • @Zebrafish: "*When the docs say "executed atomically" does it mean that calling lock() method uses a mutex every time you want to access the weak_ptr, or does it use an atomic operation?*" ... did you just ask if doing something atomically *is an atomic operation?* – Nicol Bolas May 06 '21 at 06:04
  • @NicolBolas I know this sounds dumb, but is that the same as using mutex lock? Is using weak_ptr where you don't access the resource from multiple threads unnecessarily slowing down the thread you're using ? Does it use std::atomic or std::mutex? – Zebrafish May 06 '21 at 06:10
  • 3
    @Zebrafish: What is "necessary" or "unnecessary" is [fluid](https://stackoverflow.com/q/15129263/734069). Furthermore, it's not clear what your question actually is. It seems your question is less whether atomic operations are in fact atomic but more a general disbelief that the C++ standard library would have any threading protections on such a type. "*Does it use std::atomic or std::mutex?*" That's an implementation detail. The operation is atomic; how that gets achieved isn't your business. – Nicol Bolas May 06 '21 at 06:15
  • @selbie `expired` does have some uses, for example I've used it to remove all expired pointers from a list in a cache – Alan Birtles May 06 '21 at 06:35
  • *Is using weak_ptr where you don't access the resource from multiple threads unnecessarily slowing down the thread you're using ?* Is this your main concern here? I don't really understand what you're wondering. Access to the control block will never have a performance impact on acces to the object. Synchronizing access to the object is your responsibility, not the `shared_ptr`s. – super May 06 '21 at 06:42

1 Answers1

0

The relevant C++ language concept is sequencing. Typically, one operation is sequenced before or sequenced after another operation. "Executed atomically" in this case means that nothing is sequenced after expired() but sequenced before shared_ptr<T>(*this).

In the end, this is really a compiler guarantee. The library of your implementation knows how to get the compiler to do the right thing. Locks in the standard library are similar; they must also make sure that the compiler does the right thing. It's really up to the library writer to decide exactly how the two are implemented.

The performance is likely implementation-dependent, but this is a pretty common class. That means implementors will care about performance.

MSalters
  • 173,980
  • 10
  • 155
  • 350