David Drysdale, in "Effective Rust" about smart pointers

I just read the section about smart pointers, and might have found three issues:

First, they say in Item 8: Familiarize yourself with reference and pointer types - Effective Rust about Weak<T>

The underlying item is dropped when the strong reference count drops to zero, but the bookkeeping structure is dropped only when the weak reference count also drops to zero.

That statement is a bit weak: "dropped" is here used twice, but not explained in detail, and I think the "bookkeeping structure" is not distinct from the actual payload, so the explanation is a bit unclear. Can you explain it in some more detail, e.g. when does the actual de-allocation occur?

Next tiny issue is

An Rc on its own gives you the ability to reach an item in different ways,

Here, I think the singular "An" is used wrong. For "different ways" we would need more than one single RC? How could we rephrase the statement?

Last issue or question is to the diagram Figure 1-8: Why have b1 and b2 to contain and use two pointers each, one pointing to the borrow count, and one to the payload. The borrow count should have a fixed size (of usize) so we know the offset to the payload and would need only one pointer each?

It is distinct. Roughly speaking, the bookkeeping structure is the one that manages counters and allocation, and the payload is inside that allocation, so it can be dropped in-place (leaving the allocation essentially in uninitialized state) before actual deallocation.

1 Like

I think the explanation of Arc is misleading. It makes it sound like there are two separate allocations which is how std::shared_ptr works in C++ (which has some advantages in unusual situations). You are right in that the bookkeeping part (two words) occupy the first 16 bytes of the allocation (on a machine with 8-byte usize).

RefCell isn't a smart pointer. Ref implements a kind of projection with a map function here that requires the two pointers shown in the illustration. It's not really a big deal to have two items in Ref because you are going to compute them both anyway and they are short-lived values that may not even end up on the "stack" (or wherever local values are stored ...).

2 Likes

When the strong count of a Rc<T> falls to zero, T's destructor is called.

When the weak count falls to zero, the allocation that contains the counters is deallocated.

Currently, the allocation that holds the counters also holds the T. If you're wondering how the T can be destructed in place, that's drop_in_place. Recall that a destructor is more general than a Drop implementation.

Perhaps...

Cloning Rc<T>s gives you the ability to reference an item from different owners, but you can usually only get a shared reference (&T) to the item. You can use get_mut to get a &mut T, but only if there are no other extant Rcs or Weaks of the same item. That's hard to arrange, so Rc is often combined with RefCell, resulting in an Rc<RefCell<T>>.

4 Likes

Do you mean freed or deallocated rather than called?

Yes, thanks. Fixed.

In case this simplifies it, suppose you have a b: Box<Option<T>>.

Compare these operations

  • *b = None;, which drops the inner T item but doesn't drop the box.
  • drop(b) which drops the whole box (including the inner item if it's not already None).

That distinction is essentially the same one as being discussed, though of course the tracking is more complex in an Arc than a Box.

4 Likes