Skip to content

← Smart Pointers and Interior Mutability step 6 of 26

Medium Primitives

Inside `Rc`: `strong_count`, `ptr_eq`, `get_mut`, `try_unwrap`

Interpret a small script over a set of named Rc<i64> handles and return the log it produces.

pub fn ownership_trace(script: Vec<String>) -> Vec<String>

Keep the handles in a BTreeMap<String, Rc<i64>>. Each line is one command; each command appends exactly one line to the log.

command effect log line
new <n> <v> create a fresh Rc<i64> named <n> <n>=1
clone <src> <dst> <dst> becomes another handle to <src>‘s value <dst>=<strong count>
count <n> <n>=<strong count>
read <n> <n> is <value>
drop <n> forget the handle <n> drop <n> -> <count that remains>
get_mut <n> <d> add <d> to the value if <n> is the only handle get_mut <n> ok / get_mut <n> shared
unwrap <n> Rc::try_unwrap — take the value if <n> is the only handle unwrap <n> ok <value> / unwrap <n> shared
into_inner <n> Rc::into_inner — same question, different answer shape into_inner <n> ok <value> / into_inner <n> none
ptr_eq <a> <b> compare both ways ptr_eq <a> <b> ptr=<bool> val=<bool>

When a named handle does not exist, log the same shape with ? in place of the number: <n>=?, <n> is ?, drop <n> -> ?, get_mut <n> ?, unwrap <n> ?, into_inner <n> ?, ptr_eq <a> <b> ?. Any line that is not a command at all logs ?.

Two details that decide several test cases:

  • unwrap on a shared handle gets the Rc back in the Err, and the named handle must survive — put it back in the map.
  • into_inner on a shared handle returns None and consumes the handle anyway. That handle is gone; the count of the others goes down.

Why these are associated functions

You call Rc::strong_count(&a), not a.strong_count(). That is not an oversight and it is not stylistic. An Rc<T> derefs to T, so a.foo() resolves to T‘s foo — and if Rc had inherent methods, every one of them would shadow a method of the same name on whatever you wrapped. Imagine Rc<MyType> where MyType::clone quietly stopped being reachable.

So Rc‘s own operations are all spelled as associated functions taking the handle explicitly, leaving the entire method namespace to T. The same applies to Box::leak, Rc::ptr_eq, Rc::get_mut — and once you know the reason, the API stops feeling arbitrary. It is the price of Deref, and every smart pointer pays it.

ptr_eq versus ==

Rc::ptr_eq(&a, &b) asks “are these the same allocation?”. a == b asks “do these values compare equal?”. Those are different questions and one test case exists purely to separate them: two Rcs created independently from the same number are ptr_eq == false and == true.

Identity is the question you want when you are asking whether two handles into a graph refer to the same node. Equality is the question you want when comparing contents. Reaching for the wrong one produces bugs that are very hard to see, because in most of your test data they agree.

“Can I ever mutate through an Rc without a RefCell?”

Yes — when you are provably the only owner. That is what the last three commands are about, and the three answers differ in useful ways:

  • Rc::get_mut(&mut rc) -> Option<&mut T>Some when the count is 1, so you keep the Rc and mutate in place.
  • Rc::try_unwrap(rc) -> Result<T, Rc<T>> — consumes the handle. Ok(value) when it was the last one; Err(rc) hands it back untouched when it was not.
  • Rc::into_inner(rc) -> Option<T> — consumes the handle either way, and reports failure as None. It is try_unwrap for the case where you did not want the handle back.

Notice the shape of the starter’s bug:

error[E0308]: mismatched types
  |     match Rc::get_mut(rc) {
  |                       ^ types differ in mutability
  = note: expected mutable reference `&mut Rc<_, _>`
                     found reference `&Rc<i64>`

get_mut needs &mut on the handle, not on the value. That is the whole trick: an exclusive borrow of the handle plus a strong count of 1 together prove nobody else can be looking, which is exactly the proof &mut T requires. Change one call from get to get_mut and it compiles.

One trap worth knowing before it bites you in three items’ time: a live Weak blocks get_mut even when the strong count is 1. You get a mystifying None with a single handle in sight. try_unwrap and into_inner, by contrast, only care about strong references.

Loading visualization…