Today I explored Rc, the Reference Counted Smart Pointer in Rust. This is where ownership gets interesting—sometimes a value might need multiple owners, and Rc provides exactly that functionality.
🔄 Why Rc?
Normally, ownership in Rust is strict: only one owner exists for any given value. But in real-world cases, multiple parts of a program may need to share access. For example, in graph structures, multiple edges may point to the same node. That node shouldn’t be deallocated until all edges stop pointing to it.
Rc<T>
keeps track of the number of references to a value. Once all references are gone, the value is safely cleaned up.
👉 Think of Rc like a TV in a family room: everyone can watch while they’re in the room, and only when the last person leaves is the TV turned off.
⚠️ Rc works only in single-threaded programs. For multithreading, Rust provides Arc<T>
(Atomic Reference Counting).
📝 Example: Shared Ownership in Lists
Let’s revisit the cons list example. If we use Box<T>
for ownership, we hit a problem:
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a)); // ❌ Error: a was moved
}
This fails because a
is moved into b
, so it can’t also be used in c
.
✅ Fixing with Rc
By using Rc, we enable multiple ownership:
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
Here, Rc::clone(&a)
increments the reference count instead of deeply copying the data. Now a
, b
, and c
all share ownership.
📊 Tracking Reference Counts
We can observe how reference counts change with Rc::strong_count
:
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
Output:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
The reference count goes up with every Rc::clone
and down when a reference goes out of scope. When it finally reaches 0, the data is deallocated.
🚫 Why Rc Only Allows Immutable References
Rc<T>
lets you share ownership via immutable references. If multiple owners could also mutate the data, Rust’s borrowing rules would be violated (e.g., multiple mutable borrows at once).
But sometimes, we do want shared mutable access. That’s where the interior mutability pattern comes in, using RefCell<T>
alongside Rc<T>
. I’ll explore this in the next session.
🧠 Summary
-
Rc<T>
enables multiple ownership through reference counting. - Cloning with
Rc::clone
increments the count instead of deep-copying. - When the last reference is dropped, the data is freed.
- Best for single-threaded scenarios where shared ownership is needed.
- For mutation, combine with
RefCell<T>
(coming up next!).
🚀 That wraps up Day 30 of #100DaysOfRust! Today I learned how Rc<T>
provides shared ownership and safe cleanup in Rust.
Tomorrow: Interior Mutability with RefCell.
Top comments (0)