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.
Basically, a "weak_ptr" is an ordinary "T*" pointer that lets you RECOVER a strong reference, i.e. "shared_ptr", later in the code.
Just like an ordinary T*, the weak_ptr doesn't do any reference-counting. Internally, to support reference-counting on an arbitrary type T, the STL (or any other library implementing this kind of logic) creates a wrapper object we'll call "Anchor". "Anchor" exists solely to implement the reference count and "when count is zero, call delete" behavior we need.
In a strong reference, the shared_ptr implements its copy, operator=, constructor, destructor, and other pertinent APIs to update "Anchor"'s reference count. This is how a shared_ptr ensures that your "T" lives for exactly as long as somebody is using it. In a "weak_ptr", those same APIs just copy the actual Anchor ptr around. They do NOT update reference counts.
This is why the most important APIs of "weak_ptr" are "expired" and the poorly-named "lock". "Expired" tells you if the underlying object is still around- i.e. "Has it already deleted itself because all strong references went out of scope?". "Lock" will (if it can) convert the weak_ptr to a strong reference shared_ptr, restoring reference-counting.
BTW, "lock" is a terrible name for that API. You aren't (just) invoking a mutex, you're creating a strong reference from a weak one, with that "Anchor" acting. The biggest flaw in both templates is that they did not implement operator->, so to do anything with your object you have to recover the raw "T*". They did this mostly to support things like "shared_ptr", because primitive types don't support the "->" operator.
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
andweak_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:shared_ptr
instances pointing to the object.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 aweak_ptr
, the library atomically checks the "use count", and if it's > 0 increments it. If that succeeds you get yourshared_ptr
. If the "use count" was already zero you get an emptyshared_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 oneweak_ptr
to some object, and they are reset at the same time in concurrent threads. Let's say theshared_ptr
comes first. It decreases the "use count" to zero, and begins executing the deleter. Now theweak_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_ptr
s 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 singleshared_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/assigningshared_ptr
instances).