Why this matters
Rust's ownership rules prevent dangling pointers and double frees, but they do not make memory leaks impossible. With Rc, two values that point at each other keep each other's count above zero forever, and neither is ever freed. This happens naturally in trees with parent pointers, doubly linked lists and graphs. The tool for breaking such cycles is Weak<T>: a reference that can see a value without keeping it alive.
Creating a leak on purpose
use std::cell::RefCell;
use std::rc::Rc;
struct Node {
name: &'static str,
next: RefCell<Option<Rc<Node>>>,
}
impl Drop for Node {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn main() {
let a = Rc::new(Node { name: "a", next: RefCell::new(None) });
let b = Rc::new(Node { name: "b", next: RefCell::new(Some(Rc::clone(&a))) });
*a.next.borrow_mut() = Some(Rc::clone(&b));
println!("a strong = {}, b strong = {}", Rc::strong_count(&a), Rc::strong_count(&b));
println!("end of main");
}
a strong = 2, b strong = 2
end of main
No "dropping" lines: neither node is ever freed. At the end of main, the variables a and b are dropped, which takes each count from 2 to 1, but the remaining count comes from the other node, which is itself alive only because of the first:
variable a --X (dropped at end of main)
Node a [strong 1] --next--> Node b [strong 1]
^ |
+-----------next-----------+
variable b --X
Rust considers leaking memory safe (it cannot cause undefined behavior), so the compiler does not stop this. In a long-running program, such leaks accumulate.
Weak: a non-owning reference
Rc::downgrade(&rc) creates a Weak<T>. It points at the same allocation but increments a separate weak count, which does not keep the value alive. To use it, call upgrade(), which returns Option<Rc<T>>: Some if the value is still alive, None if it has been dropped:
use std::rc::Rc;
fn main() {
let strong = Rc::new(String::from("data"));
let weak = Rc::downgrade(&strong);
println!("strong {}, weak {}", Rc::strong_count(&strong), Rc::weak_count(&strong));
match weak.upgrade() {
Some(s) => println!("still here: {s}"),
None => println!("gone"),
}
drop(strong);
println!("after drop: {:?}", weak.upgrade());
}
strong 1, weak 1
still here: data
after drop: None
When the strong count reaches zero the value is dropped, even if weak references remain. (The allocation holding the counts stays until the weak count reaches zero too, so upgrade can safely report None.) A Weak can never dangle: you cannot reach the value without upgrading, and upgrading checks.
Breaking cycles: decide who owns whom
The fix for cycles is a design decision: in each pair of mutual references, one direction owns (Rc) and the other merely refers (Weak). In a tree, parents own their children, and children refer weakly to their parent:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
impl Drop for Node {
fn drop(&mut self) {
println!("dropping {}", self.value);
}
}
fn main() {
let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
{
let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]) });
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
let parent_value = leaf.parent.borrow().upgrade().map(|p| p.value);
println!("leaf's parent = {:?}", parent_value);
println!("branch strong {}, weak {}", Rc::strong_count(&branch), Rc::weak_count(&branch));
println!("leaf strong {}, weak {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
}
println!("after branch scope: leaf's parent = {:?}", leaf.parent.borrow().upgrade().map(|p| p.value));
println!("leaf strong {}", Rc::strong_count(&leaf));
}
leaf's parent = Some(5)
branch strong 1, weak 1
leaf strong 2, weak 0
dropping 5
after branch scope: leaf's parent = None
leaf strong 1
dropping 3
When branch goes out of scope, its strong count drops to zero (the leaf's reference to it is weak), so the branch is dropped, which drops its children vector, which decrements the leaf's count. The leaf survives because the variable leaf still owns it, and its parent link now upgrades to None. Everything is freed in the end.
branch (Rc, owns) --children--> leaf
^ |
+------ parent (Weak) ---------+
The same rule applies elsewhere: in a doubly linked list, next is strong and prev is weak; in an observer pattern, the subject holds Weak references to observers so it does not keep them alive.
Alternatives worth knowing
Rc<RefCell<...>> graphs work, but they are verbose and push errors to run time. Many Rust programs instead store all nodes in a Vec and refer to them by index (usize): no reference counting, no cycles to leak, cache-friendly, and borrow-checker friendly. For graphs with many cross-links, an arena with indexes is often the cleaner design.
Common mistakes
- Using
Rcin both directions: leaks. Decide ownership; make back-referencesWeak. - Unwrapping
upgrade(): the value may be gone. HandleNone. - Forgetting that
Weak::new()points at nothing:upgrade()on it always returnsNone; it is the natural initial value for "no parent yet". - Holding an upgraded
Rclonger than needed: it is a strong reference and keeps the value alive while you hold it.
Recap
Rccycles keep each other alive forever: a memory leak (safe, but a bug).Weak<T>(fromRc::downgrade) refers to a value without owning it;upgrade()givesOption<Rc<T>>.- The value is dropped when the strong count reaches zero, regardless of weak references.
- Break cycles by choosing an owning direction (
Rc) and a referring direction (Weak): parents own children. - Index-based arenas are a simpler alternative for complex graphs.
Going further: read the Rust Book chapter 15.6, "Reference Cycles Can Leak Memory". Then build a three-level tree and print strong_count/weak_count for each node as you drop levels.