&* on a Weak<T>, Weak, RC, Drop, Free

  1. Sometimes, we want to hash a Rc by ADDRESS, not by VALUE. So for x: Rc<T>, we can do &*x

  2. Now this gets fun. Suppose we have x: Weak<T>, can we still do &*x ? Is this behaviour when an object has strong_count = 0, but weak_count > 0 ?

  3. If T has a Drop on it, the drop function is called when strong_count == 0, even if weak_count > 0 right?

  4. If strong_count == 0, but weak_count > 0, when is the memory freed? (Because if it’s freed when weak_count > 0, it seems &*x is going to be undefined behaviour / pointing at random object; but if it’s not freed until weak_count == 0, then it seems like we are wasting memory.)

2, 3, 4 = three separate questions. To help discussion, please label answers when answering.

Weak does not implement Deref. So it seems that on your second question the answer is “no, we can’t”.

1 Like

@Cerberuser : You’re right, we can’t do &*x for x: Weak<T>, so the assumption of my question is wrong.

I’m still not sure about 3 & 4 for the following reason:

In order for a Weak<T> to know “there’s no longer a live object here”, it seems that we can NOT free the memory (since otherwise some other object can use it). In this case, when does drop & free occur?

Yes.

The value is accessed by calling upgrade on the Weak pointer, which returns an Option<Arc>. Returns None if the value has since been dropped.

This, in response to #4, ignores the question of “when is the memory freed.” How does the Weak know to return a None or a Some if the memory is already freed.

There is not one memory location to be freed, but two. One of them stores counters and should be treated as metadata, destroyed only when the last smart pointer (regardless of weakness) goes out of scope, and that’s the part necessary to check whether the real data still lives. And real data are stored in another place in memory, accessed only if the counters allow Weak to do so: if the strong counter reaches zero, Weak will never refer to this region.

2 Likes

My confusion was a result of failing to understand:

Rc<T> has a memory layout that looks something like:

loc 1 in memory: sizeof(T): stores actual T
loc 2 in memory: (i64: ttl_count, i64: strong_count)

when loc2.strong_count hits 0, loc1 is dropped + freed
when loc2.ttl_count hits 0, loc2 is freed

Weak/Strong makes perfect sense under this model now.

Thanks!

Rc<T> heap-allocates an RcBox<T>, that is, a struct with three elements in it: the strong_count and weak_count counters, and an inlined T.

When strong_count reaches 0, ptr::drop_in_place::<T>(&mut inlined_T), thus recursively dropping the memory owned by T, but not freeing T itself (it can’t, since the remaining Weak<T> need to read the weak_count value.


Example

use ::std::rc::{Rc, Weak};

let x1: Rc<String> = Rc::new(String::from("Foo"));
let x2: Rc<String> = x1.clone();
let w: Weak<String> = Rc::downgrade(&x1);

Here is a diagram representing the memory layout:

rc_string

// 1.
mem::drop(x1);
// 2.
mem::drop(x2);
// 3.
mem::drop(w);
  1. x1 is dropped
    strong_count > 1, so it is just decremented and nothing else happens.

  2. x2 is dropped
    strong_count == 1 so ptr::drop_in_place(&mut inner) is called, and the heap-allocated str bytes are freed. Then strong_count is decremented, and since weak_count > 0, the RcBox<String> is still around (although inner: String is now invalid data).

  3. w is dropped
    weak_count is decremented and goes down to 0, so it checks if strong_count == 0. It is the case, so the RcBox<String> is freed.


Conclusion

The memory (RcBox<T>) is only freed when all the counters drop (pun intended :stuck_out_tongue:) down to 0, but the data “stops being owned” (T is dropped) as soon as strong_count reaches 0, making a Rc <-> Weak cycle not leak memory.

Note: this behavior is exactly the same for Arc, the only thing that changes is the way the counters are incremented / decremented (using atomic operations to avoid breaking coherence).

8 Likes

I don’t want to come off as pedantic given the exceptional quality of your response, but I have one confusion left:

Is it true that:

Sizeof(RcBox<T>) = usize + usize + sizeof(T) ?

From the diagram, it looks like the last field of the RcBox<T> is a pointer, and the actual contents of T is stored elsewhere on the heap.

However, based on your text (and explanation for drop in place rather than drop), it seems the contents of T are stored immediately after the weak_count.

@Yandros : PS If you ever want to create a “Rust Illustrated” Kickstarter/Pateron, I’d be interested in contributing.

Yes

Yes, that’s my bad, the diagram should have had T = Box<U> written on it, because that’s the particular case (Rc<Box<T>>) I have used in my example to show what the different drops do. Had it been just a Rc<U>, there wouldn’t have been a second indirection.

This is the same difference between Rc<String> and Rc<str>, for instance :wink:

EDIT: now the diagram uses Rc<String> instead of Rc<Box<U>>

1 Like

Everything is crystal clear now. Thanks again for your detailed response above. It belongs in some “rust answer hall of fame”

1 Like

I have been thinking about writing a Rust guide, but couldn’t really figure out what I could add that would make it actually different (and hence potentially useful) from the other guides / book out there.

Illustrations is a good idea, although not my strongest area. But since the one above seems to have had some success, I may give it a try :smiley:

Anyways, thanks for the offer, I appreciate it :slight_smile:

1 Like

Do you have any interest in Vector Clocks, Paxos, Raft, CRDTs ? I think there’s a need for a well written + illustrated ebook explaining how to implement these classical distributed algorithms in Rust.

In particular, I think it’ll be interesting to see how traits, typestates, lifetime checker interacts with these algorithms.