How Does Weak_Ptr Work

How does weak_ptr work?

shared_ptr uses an extra "counter" object (aka. "shared count" or "control block") to store the reference count.
(BTW: that "counter" object also stores the deleter.)

Every shared_ptr and weak_ptr contains a pointer to the actual pointee, and a second pointer to the "counter" object.

To implement weak_ptr, the "counter" object stores two different counters:

  • The "use count" is the number of shared_ptr instances pointing to the object.
  • The "weak count" is the number of weak_ptr instances pointing to the object, plus one if the "use count" is still > 0.

The pointee is deleted when the "use count" reaches zero.

The "counter" helper object is deleted when the "weak count" reaches zero (which means the "use count" must also be zero, see above).

When you try to obtain a shared_ptr from a weak_ptr, the library atomically checks the "use count", and if it's > 0 increments it. If that succeeds you get your shared_ptr. If the "use count" was already zero you get an empty shared_ptr instance instead.


EDIT: Now, why do they add one to the weak count instead of just releasing the "counter" object when both counts drop to zero? Good question.

The alternative would be to delete the "counter" object when both the "use count" and the "weak count" drop to zero. Here's the first reason: Checking two (pointer sized) counters atomically is not possible on every platform, and even where it is, it's more complicated than checking just one counter.

Another reason is that the deleter must stay valid until it has finished executing. Since the deleter is stored in the "counter" object, that means the "counter" object must stay valid. Consider what could happen if there is one shared_ptr and one weak_ptr to some object, and they are reset at the same time in concurrent threads. Let's say the shared_ptr comes first. It decreases the "use count" to zero, and begins executing the deleter. Now the weak_ptr decreases the "weak count" to zero, and finds the "use count" is zero as well. So it deletes the "counter" object, and with it the deleter. While the deleter is still running.

Of course there would be different ways to assure that the "counter" object stays alive, but I think increasing the "weak count" by one is a very elegant and intuitive solution. The "weak count" becomes the reference count for the "counter" object. And since shared_ptrs reference the counter object too, they too have to increment the "weak count".

A probably even more intuitive solution would be to increment the "weak count" for every single shared_ptr, since every single shared_ptr hold's a reference to the "counter" object.

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

How does a weak_ptr know that the shared resources has expired?

The control block allocated when a shared_ptr is created from a plain pointer contains both the reference counter for the object and the pointer to the object itself and the custom deleter object if any. When that reference counter reaches zero the object is released and the pointer is set to null. So, when the object reference counter is zero it means that the object is gone.

For x86 and x86-64 they use atomic operations and no explicit locking (no mutex or spinlock). The trick of the implementation is a special lock-free (code language for busy spin) function atomic_conditional_increment that only increments the object reference counter if it is not zero. It is used in the implementation of weak_ptr::lock function to cope with a race when more than one thread tries to create a shared_ptr from the same weak_ptr with object reference counter being zero. See http://www.boost.org/doc/libs/1_52_0/boost/smart_ptr/detail/sp_counted_base_gcc_x86.hpp

The control block itself is shared between shared_ptr's and weak_ptr's and has another reference counter for itself, so that it stays alive till the last reference to it is released.

When a shared_ptr is reassigned it points to another control block, so that a control block only ever points to one same object. In other words, there is no replacement of one object with another in the control block.

When is std::weak_ptr useful?

A good example would be a cache.

For recently accessed objects, you want to keep them in memory, so you hold a strong pointer to them. Periodically, you scan the cache and decide which objects have not been accessed recently. You don't need to keep those in memory, so you get rid of the strong pointer.

But what if that object is in use and some other code holds a strong pointer to it? If the cache gets rid of its only pointer to the object, it can never find it again. So the cache keeps a weak pointer to objects that it needs to find if they happen to stay in memory.

This is exactly what a weak pointer does -- it allows you to locate an object if it's still around, but doesn't keep it around if nothing else needs it.

Does weak_ptr work alongside unique_ptr?

As I understand it std::weak_ptr is used as a safe way of referencing memory referenced by shared_ptrs which may have been deallocated.

You understand wrong. std::weak_ptr allowes to access object, which ownership is maintained by std::shared_ptr without sharing it. Now when you really understand what std::weak_ptr does you should understand that your question about std::unique_ptr does not make any sense.

Why does shared_ptr needs to hold reference counting for weak_ptr?

The reference count controls the lifetime of the pointed-to-object. The weak count does not, but does control (or participate in control of) the lifetime of the control block.

If the reference count goes to 0, the object is destroyed, but not necessarily deallocated. When the weak count goes to 0 (or when the reference count goes to 0, if there are no weak_ptrs when that happens), the control block is destroyed and deallocated, and the storage for the object is deallocated if it wasn't already.

The separation between destroying and deallocating the pointed-to-object is an implementation detail you don't need to care about, but it is caused by using make_shared.

If you do

shared_ptr<int> myPtr(new int{10});

you allocate the storage for the int, then pass that into the shared_ptr constructor, which allocates storage for the control block separately. In this case, the storage for the int can be deallocated as early as possible: as soon as the reference count hits 0, even if there is still a weak count.

If you do

auto myPtr = make_shared<int>(10);

then make_shared might perform an optimisation where it allocates the storage for the int and the control block in one go. This means that the storage for the int can't be deallocated until the storage for the control block can also be deallocated. The lifetime of the int ends when the reference count hits 0, but the storage for it is not deallocated until the weak count hits 0.

Is that clear now?

What Happens to a weak_ptr when Its shared_ptr is Destroyed?

A std::shared_ptr is created using two pieces of memory:

  • A resource block: This holds the pointer to the actual underlying data, e.g. 'int*'

  • A control block: This holds information specific to a shared_ptr, for example reference counts.

(Sometimes these are allocated in a single chunk of memory for efficiency, see std::make_shared)

The control block also stores reference counts for weak_ptr. It will not be deallocated until the last weak_ptr goes out of scope (the weak pointer reference count drops to zero).

So a weak_ptr will know that it's expired because it has access to this control block, and it can check to see what the reference count is for a shared_ptr

What variables does weak_ptr hold?

An unintrusive shared pointer implementation usually contains a pointer to some dynamically-allocated "state", which counts how many references there are to the original object.

When a shared pointer is copied, the copy gets the same pointer to the same "state", and the count inside the "state" is incremented to indicate that there are now two shared pointers sharing the resource.

When a shared pointer is destroyed, it decrements the counter to indicate that there is now one fewer pointer sharing the resource. If this results in the counter reading zero, the resource is destroyed.

A weak pointer also has a pointer to this "state", but it doesn't increment or decrement the counter. When asked, it will construct a shared pointer using the same state, but only if the count is not zero. If the count is zero, the last shared pointer already destroyed the resource, and we can't get access to it any more.

What's interesting is that you also need logic like this to control the lifetime of the "state" object. :) (I'd imagine that's implemented using a second counter, which both shared_ptr and weak_ptr do increment, but don't quote me on that.)

(your data)         (ref. counters)
║ ║
[resource] [state]
┆ │ │ │ │ │
┆ │ └─[shared_ptr]───┘ │ │
┆ └───[shared_ptr]─────┘ │
└┄┄┄┄┄┄┄[weak_ptr]────────┘

Of course, what the private section of any particular std::weak_ptr implementation exactly looks like is up to the person who wrote it.

Incidentally, the diagram kind of shows why you shouldn't construct a shared_ptr from a raw pointer, if you suspect that the resource it points to may already be managed by shared_ptr(s) elsewhere: you'd get a second, unrelated "state" object, your counters will be wrong, and your resource may be destroyed prematurely (and will definitely be destroyed twice, if such a notion exists), causing mayhem.

Why can't a weak_ptr be constructed from a unique_ptr?

std::weak_ptr can't be used unless you convert it to std::shared_ptr by the means of lock(). if the standard allowed what you suggest, that means that you need to convert std::weak_ptr to unique in order to use it, violating the uniqueness (or re-inventing std::shared_ptr)

In order to illustrate, look at the two pieces of code:

std::shared_ptr<int> shared = std::make_shared<int>(10);
std::weak_ptr<int> weak(shared);

{
*(weak.lock()) = 20; //OK, the temporary shared_ptr will be destroyed but the pointee-integer still has shared to keep it alive
}

Now with your suggestion:

std::unique_ptr<int> unique = std::make_unique<int>(10);
std::weak_ptr<int> weak(unique);

{
*(weak.lock()) = 20; //not OK. the temporary unique_ptr will be destroyed but unique still points at it!
}

That has been said, you may suggest that there is only one unique_ptr, and you still can dereference weak_ptr (without creating another unique_ptr) then there is no problem. But then what is the difference between unique_ptr and shared_ptr with one reference? or moreover, what is the difference between a regular unique_ptr and C-pointers an get by using get?

weak_ptr is not for "general nonowning resources", it has a very specific job - The main goal of weak_ptr is to prevent circular pointing of shared_ptr which will make a memory leak. Anything else needs to be done with plain unique_ptr and shared_ptr.



Related Topics



Leave a reply



Submit