2

If we look at the following code (playground link):

use std::cell::RefCell;
use std::rc::Rc;

struct Person {
    name: String,
    parent: Option<Rc<RefCell<Person>>>,
    child: Option<Rc<RefCell<Person>>>,
}

impl Drop for Person {
    fn drop(&mut self) {
        println!("dropping {}", &self.name);
    }
}

pub fn main() {
    let anakin = Rc::new(RefCell::new(Person {
        parent: None,
        child: None,
        name: "anakin".to_owned(),
    }));

    let luke = Rc::new(RefCell::new(Person {
        parent: None,
        child: None,
        name: "luke".to_owned(),
    }));

    luke.borrow_mut().parent = Some(anakin.clone());
    anakin.borrow_mut().child = Some(luke.clone());

    println!("anakin {}", Rc::strong_count(&anakin));
    println!("luke {}", Rc::strong_count(&luke));
}

When the code runs, the message from the drop implementation is not printed, because this code is supposed to leak memory.

When the code finishes the count for both luke and anakin is supposed to end up at 1 rather than 0 (which is when the managed heap data would be cleaned up).

Why does the count not end up being 0? I would have thought the following sequence would happen:

  1. we start with two data locations in heap, one pointed to by luke and anakin.child, the other pointed to by anakin and luke.parent. The object luke owns luke.parent. The object anakin owns anakin.child

  2. luke goes out of scope, which means its members which it owns are also dropped. So luke and luke.parent drop. Both are Rcs, so the ref count for both the memory locations therefore goes down to 1

  3. anakin is dropped, causing the Rc object anakin and anakin.child to drop, which causes ref count for both memory locations to go down to 0.

This is where the heap data should be cleaned up? Are the members of an object not dropped when it is dropped?

When I remove or comment the lines connecting luke and anakin via borrow_mut, the drop happens as expected and the messages are correctly printed.

abhijat
  • 535
  • 6
  • 12

1 Answers1

2
  1. luke goes out of scope, which means its members which it owns are also dropped. So luke and luke.parent drop. Both are Rcs, so the ref count for both the memory locations therefore goes down to 1

This is the step where you are misunderstanding.

Are the members of an object not dropped when it is dropped?

Exactly. The count that is decreased is the one directly associated with the data that the Rc points. When luke goes out of scope, the Rc goes out of scope, and the count of things referencing luke's RefCell decreases, the code knows nothing about luke.parent.

In your code, after

luke.borrow_mut().parent = Some(anakin.clone());
anakin.borrow_mut().child = Some(luke.clone());

there are 4 Rc objects (2 on stack, 2 on heap), and 2 RefCell objects on the heap with counts associated with them. Each RefCell has a count of 2 as you've seen, because there are 2 Rcs referencing each RefCell.

When anakin drops, the count for it's RefCell decreases, so you then have the luke RefCell on the heap with a count of 2 and the anakin RefCell with a count of 1.

When luke drops, that then decreases the count of that RefCell, so each one now has a count of 1. You end up with no Rc values on the stack that reference the RefCell, but each RefCell references the other RefCell, so there is no way for Rust to know that they are safe to be dropped.

I can't quite tell from your question, but this is absolutely an expected limitation of Rc type when used with RefCell because it allows for the introduction of cycles in the ownership of objects.

Your code is nearly the minimum reproducible example of an Rc cycle: What is a minimal example of an Rc dependency cycle?

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251