Rust’s ownership system prevents dangling pointers and double frees — but not memory leaks. The one case where Rust safe code can leak memory is a reference cycle: two or more Rc<T> values pointing to each other, creating a loop that prevents the reference count from ever reaching zero.
Understanding cycles and how to prevent them with Weak<T> is essential for writing correct Rust programs that involve shared ownership.
What Is a Reference Cycle?
When two Rc<T> values reference each other, they create a cycle:
a → b → a → b → ...
Even after a and b go out of scope, their Rc strong counts never reach zero (each holds a reference to the other). The drop destructor is never called. The memory leaks for the lifetime of the process.
Creating a Cycle (And Why It Leaks)
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
use List::*;
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, tail) => Some(tail),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a count before b: {}", Rc::strong_count(&a)); // 1
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a count after b: {}", Rc::strong_count(&a)); // 2
println!("b count: {}", Rc::strong_count(&b)); // 1
// Create the cycle: a's tail now points to b
if let Some(tail) = a.tail() {
*tail.borrow_mut() = Rc::clone(&b);
}
println!("a count with cycle: {}", Rc::strong_count(&a)); // 2
println!("b count with cycle: {}", Rc::strong_count(&b)); // 2
// At end of scope:
// b drops: strong_count(b) → 1 (a still holds it), memory NOT freed
// a drops: strong_count(a) → 1 (b still holds it), memory NOT freed
// LEAK: ~48 bytes per Cons node never reclaimed
// This would stack overflow (infinite recursion):
// println!("a next: {:?}", a.tail());
}
After the function returns, both a and b have strong count 1, so neither is freed.
Detecting Cycles
The standard library doesn’t provide cycle detection. In debug builds, you can reason about counts:
use std::rc::Rc;
fn check_for_leak<T>(rc: &Rc<T>, expected_count: usize) {
let count = Rc::strong_count(rc);
if count > expected_count {
eprintln!("Warning: Rc strong_count={} (expected ≤{}), possible cycle",
count, expected_count);
}
}
For production use, tools like Valgrind or address sanitizers can detect leaked Rc values.
Breaking Cycles with Weak<T>
Weak<T> is a non-owning reference to an Rc-managed value:
- It does not increment the strong count
- It does not prevent the value from being dropped
- To access the value, you call
.upgrade()which returnsOption<Rc<T>>
The pattern: owners use Rc<T>, back-references use Weak<T>.
use std::rc::{Rc, Weak};
fn main() {
let strong = Rc::new(String::from("hello"));
let weak: Weak<String> = Rc::downgrade(&strong); // Create weak ref
println!("strong count: {}", Rc::strong_count(&strong)); // 1
println!("weak count: {}", Rc::weak_count(&strong)); // 1
// Upgrade to access the value
match weak.upgrade() {
Some(s) => println!("Value: {}", s), // "hello"
None => println!("Dropped"),
}
drop(strong); // Strong count → 0, value is freed
// After drop, upgrade returns None
match weak.upgrade() {
Some(_) => println!("Still alive"),
None => println!("Value was dropped"), // This prints
}
}
Cycle-Free Tree with Weak
The canonical example: a tree where parents own children, but children hold non-owning references back to their parents:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // Non-owning upward link
children: RefCell<Vec<Rc<Node>>>, // Owning downward links
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
value,
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn add_child(parent: &Rc<Node>, child: Rc<Node>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.children.borrow_mut().push(child);
}
}
fn main() {
let root = Node::new(1);
let child = Node::new(2);
let leaf = Node::new(3);
Node::add_child(&root, Rc::clone(&child));
Node::add_child(&child, Rc::clone(&leaf));
// Navigate downward (strong refs)
for c in root.children.borrow().iter() {
println!("root child: {}", c.value);
}
// Navigate upward (weak refs — must upgrade)
let parent_of_leaf = leaf.parent.borrow().upgrade();
println!("leaf parent: {:?}", parent_of_leaf.as_ref().map(|p| p.value)); // Some(2)
// Reference counts
println!("root strong={} weak={}", Rc::strong_count(&root), Rc::weak_count(&root)); // 1, 0
println!("child strong={} weak={}", Rc::strong_count(&child), Rc::weak_count(&child)); // 2, 1
println!("leaf strong={} weak={}", Rc::strong_count(&leaf), Rc::weak_count(&leaf)); // 2, 0
// When `root` is dropped:
// root strong → 0 → root freed
// child strong → 1 (child var still holds it) → NOT freed yet
// leaf strong → 1 (leaf var still holds it) → NOT freed yet
// When `child` is dropped:
// child strong → 0 → child freed
// leaf strong → 0 → leaf freed
// No leaks!
}
Doubly-Linked List
A doubly-linked list needs both forward and backward links — the classic cycle-prone structure:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct ListNode<T> {
value: T,
next: Option<Rc<RefCell<ListNode<T>>>>,
prev: Option<Weak<RefCell<ListNode<T>>>>, // Weak to break cycle
}
impl<T: std::fmt::Debug> ListNode<T> {
fn new(value: T) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(ListNode {
value,
next: None,
prev: None,
}))
}
}
struct DoublyLinkedList<T> {
head: Option<Rc<RefCell<ListNode<T>>>>,
tail: Weak<RefCell<ListNode<T>>>,
len: usize,
}
impl<T: std::fmt::Debug + Clone> DoublyLinkedList<T> {
fn new() -> Self {
DoublyLinkedList { head: None, tail: Weak::new(), len: 0 }
}
fn push_back(&mut self, value: T) {
let new_node = ListNode::new(value);
match self.tail.upgrade() {
Some(old_tail) => {
new_node.borrow_mut().prev = Some(Rc::downgrade(&old_tail));
old_tail.borrow_mut().next = Some(Rc::clone(&new_node));
}
None => {
self.head = Some(Rc::clone(&new_node));
}
}
self.tail = Rc::downgrade(&new_node);
self.len += 1;
}
fn iter_forward(&self) -> Vec<T> {
let mut result = Vec::new();
let mut current = self.head.clone();
while let Some(node) = current {
result.push(node.borrow().value.clone());
current = node.borrow().next.clone();
}
result
}
}
fn main() {
let mut list = DoublyLinkedList::new();
list.push_back(1);
list.push_back(2);
list.push_back(3);
println!("{:?}", list.iter_forward()); // [1, 2, 3]
println!("Length: {}", list.len); // 3
}
The Observer Pattern Without Leaks
A common pattern where you need the subject to hold references to observers, but observers might be dropped:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
trait Listener {
fn on_change(&self, new_value: i32);
}
struct Counter {
value: i32,
listeners: Vec<Weak<dyn Listener>>, // Weak so counter doesn't keep listeners alive
}
impl Counter {
fn new() -> Self { Counter { value: 0, listeners: Vec::new() } }
fn add_listener(&mut self, listener: &Rc<dyn Listener>) {
self.listeners.push(Rc::downgrade(listener));
}
fn increment(&mut self) {
self.value += 1;
// Remove dropped listeners and notify live ones
self.listeners.retain(|weak| {
if let Some(l) = weak.upgrade() {
l.on_change(self.value);
true
} else {
false
}
});
}
}
struct Logger { name: String }
impl Listener for Logger {
fn on_change(&self, value: i32) {
println!("[{}] counter changed to {}", self.name, value);
}
}
fn main() {
let mut counter = Counter::new();
let l1: Rc<dyn Listener> = Rc::new(Logger { name: "L1".to_string() });
counter.add_listener(&l1);
{
let l2: Rc<dyn Listener> = Rc::new(Logger { name: "L2".to_string() });
counter.add_listener(&l2);
counter.increment(); // Both listeners notified
} // l2 dropped here
counter.increment(); // Only l1 notified, dead l2 weak ref is cleaned up
println!("Active listeners: {}", counter.listeners.len()); // 1
}
Rules for Cycle-Free Design
- Establish ownership direction: decide which node “owns” which. Parents own children; containers own elements.
- Back-references use
Weak: any reference going “upward” or “backward” in the ownership tree should beWeak. - Question every
Rc::clone: if both sides of a relationship useRc, you likely have a cycle risk. - Strong count should match your mental model: at any point, the strong count equals the number of live owners. If it’s higher than expected, a cycle may exist.
Rc vs Weak Reference Counts
use std::rc::{Rc, Weak};
fn main() {
let a = Rc::new(42);
let b = Rc::clone(&a); // strong count: 2
let w = Rc::downgrade(&a); // weak count: 1, strong count: 2
println!("strong: {}, weak: {}", Rc::strong_count(&a), Rc::weak_count(&a));
// strong: 2, weak: 1
drop(b);
println!("strong: {}, weak: {}", Rc::strong_count(&a), Rc::weak_count(&a));
// strong: 1, weak: 1
drop(a);
// strong count → 0: value is dropped (even though weak count is still 1)
println!("upgraded: {:?}", w.upgrade()); // None
}
The weak count tracks how many Weak<T> pointers exist. When strong count hits zero, the value is dropped regardless of weak count. The Weak<T> allocations themselves are cleaned up when weak count also hits zero.
Summary
| Pointer | Owns value? | Prevents drop? | Thread-safe? | Use for |
|---|---|---|---|---|
Rc<T> |
Yes | Yes | No | Shared ownership, single thread |
Weak<T> |
No | No | No | Back-references, breaking cycles |
Arc<T> |
Yes | Yes | Yes | Shared ownership, multi-thread |
Weak<Arc<T>> |
No | No | Yes | Back-references in async/multi-thread code |
The rule: Rc for forward/downward references, Weak for backward/upward references.
Comments