How does weak_ptr work?

25,150

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

Share:
25,150
Oliver Zheng
Author by

Oliver Zheng

Updated on July 05, 2022

Comments

  • Oliver Zheng
    Oliver Zheng almost 2 years

    I understand how to use weak_ptr and shared_ptr. I understand how shared_ptr works, by counting the number of references in its object. How does weak_ptr work? I tried reading through the boost source code, and I'm not familiar enough with boost to understand all the things it uses.

    Thanks.

  • Johannes Schaub - litb
    Johannes Schaub - litb about 13 years
    They don't check whether the weak count and the use count are zero, and then delete the counter object. But instead they increase the weak count by one if use count is non-zero, and then only check the weak count. Why is that?
  • Paul Groke
    Paul Groke about 13 years
    Since this is a little hard to explain with just a comment, I updated my answer.
  • Johannes Schaub - litb
    Johannes Schaub - litb about 13 years
    Perhaps the reason they only add 1 at all and not 1 for each shared_ptr is that then they only need to touch the counter object once when they need to touch it anyway to call the deleter, and not everytime they decrement the use counter?
  • Paul Groke
    Paul Groke about 13 years
    They have to touch the "counter" object anyway to update the "use count". However, updating the "weak count" as well would require either double-width CAS (which isn't available on many platforms) or a second CAS instruction (which is quite expensive). The "one for all" solution only requires one additional CAS per pointee, and that is after the pointee has been deleted. (The initialization doesn't require atomic instructions since there can be no other threads accessing the "counter" object at that time).
  • Quuxplusone
    Quuxplusone almost 9 years
    You wrote: In a "weak_ptr", those same APIs just copy the actual Anchor ptr around. They do NOT update reference counts. That's incorrect; they update the weak reference count, which manages the lifetime of the Anchor itself. Otherwise, how would you know when to free the Anchor's own memory? Also, std::shared_ptr does implement operator->; I don't know how you got the idea that it doesn't.
  • curiousguy
    curiousguy over 7 years
    STL is the abbreviation of Standard Template Library, ie containers, iterator, etc. Smart pointers are not part of it.