Why do you need a weak_count?

Why do you need a weak_count counter for Rc.? strong_count is needed to know when to free up memory. And what is the weak_count counter for?

I assume that during drop Rc changes all data inside Weak to None if weak_count > 0

When using an Rc, there are two different things that need to be cleaned up:

  1. The value inside the Rc needs to be dropped when the strong count reaches zero.
  2. The allocation that held the value needs to be deallocated when the weak count reaches zero.

This is because a Weak<T> still has a pointer to the allocation, and it needs to continue to be able to access it so that it can read the strong and weak counts. If you deallocated it when there are still weak references, then trying to upgrade them would be a use-after-free.

7 Likes

Without Weak, self-referential data types would never be dropped (one Rc holding the other Rc and vice versa, the strong_count of either one would never reach 0). From the docs:

A Weak pointer is useful for keeping a temporary reference to the allocation managed by Rc without preventing its inner value from being dropped. It is also used to prevent circular references between Rc pointers, since mutual owning references would never allow either Rc to be dropped. For example, a tree could have strong Rc pointers from parent nodes to children, and Weak pointers from children back to their parents.

1 Like

The thing that Weak needs access to is some shared data that tracks whether or not the Rc’s value still exists. After all, there needs to be some way to implement Weak::upgrade. The weak count is then necessary to determine when this shared data can be dropped. What kind of data exactly is shared for this purpose is of course another implementation detail, in practice Rust (currently) uses the strong count, not only as a way to know when the Rc’s value can be dropped, but also as an indicator to Weak pointers whether or not the value still exists and thus whether .upgrade() can succeed.

Furthermore, the strong counter and the weak counter are bundled together with the Rc’s value into a single allocation, because this allows Rc and Weak to remain small (single pointer), whilst also avoiding multiple-indirections, and it also allows keep the number of allocation and deallocation operations lower. Of course, for Rc<T> where mem::size_of::<T>() is relatively large, like e.g. Rc<[u8; 1000000]>, this trade-off has the disadvantage that the memory needed to hold the value, is not immediately freed when the value is dropped (by the last Rc being dropped), but only at a later point (when the last Weak is dropped). But for the vast majority of types, their (shallow) size is small enough that it’s a no-brainer to accept this small disadvantage, and in the worst case, you could always fall back to something like Rc<Box<[u8; 1000000]>> to avoid the problem (with the disadvantage of introducing additional indirection).

5 Likes

thank you for such a detailed explanation

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.