Rc<T>: Reference-Counted Shared Ownership
In most cases, ownership is clear: you know exactly which variable owns a given value. However, sometimes a single value needs to be owned by multiple parts of your program. For example, in a graph data structure, multiple edges might point to the same node, and logically that node is owned by all the edges that point to it.
For these cases, Oxide provides Rc<T>, a type that enables reference counting. Rc stands for "Reference Counted." An Rc<T> keeps track of how many owners a value has and only frees the value when there are no more owners.
When to Use Rc<T>
- When you have data that needs to be owned by multiple parts of your program
- When you don't know at compile time which part will finish using the data last
- In graph structures where multiple nodes reference the same data
- In single-threaded code (for multi-threaded, use
Arc<T>)
A Practical Example: Graph with Multiple Owners
Let's say we're building a graph where multiple nodes can reference the same data:
public struct Node {
public name: String,
public neighbors: Vec<Rc<Node>>,
}
fn main() {
// Create a node using Rc
let node1 = Rc { Node {
name: "Node 1",
neighbors: vec![],
} }
// Clone the Rc to create another reference (not a copy of the data)
let node2 = Rc.clone(&node1)
// Both node1 and node2 point to the same data
println!("Node 1: \(node1.name)")
println!("Node 2: \(node2.name)")
// The reference count is 2 at this point
println!("Reference count: \(Rc.strongCount(&node1))")
}
Cloning an Rc<T>
When you call Rc.clone(&rcValue), you're creating another reference to the same value, not copying the value itself. This is different from calling clone() on the value inside.
let original = Rc { String.from("hello") }
// This creates another Rc pointing to the same String
let cloned = Rc.clone(&original)
// Both original and cloned point to the same String
println!("Reference count: \(Rc.strongCount(&original))") // Prints 2
Why the explicit Rc.clone() instead of just .clone()? Because Rc.clone() is cheap—it just increments a counter—while clone() on the String inside would copy the entire string. Using Rc.clone() makes it explicit that you're doing cheap reference counting, not expensive data copying.
Reference Counting in Action
Let's trace through what happens with reference counts:
fn main() {
// rc1 points to a String, reference count = 1
let rc1 = Rc { String.from("hello") }
println!("Count after creating rc1: \(Rc.strongCount(&rc1))") // 1
{
// rc2 is a new reference to the same String, count = 2
let rc2 = Rc.clone(&rc1)
println!("Count after creating rc2: \(Rc.strongCount(&rc1))") // 2
// Inside this scope, both rc1 and rc2 are valid
println!("rc1: \(rc1), rc2: \(rc2)")
} // rc2 goes out of scope, count decrements to 1
println!("Count after rc2 goes out of scope: \(Rc.strongCount(&rc1))") // 1
} // rc1 goes out of scope, count = 0, String is dropped
Rc<T> with Structs
Here's a more practical example using structs:
public struct Cons<T> {
public head: T,
public tail: Rc<Cons<T>>?,
}
fn main() {
// Create a list: [3, [5, [10]]]
let list1 = Rc { Cons {
head: 3,
tail: Rc { Cons {
head: 5,
tail: Rc { Cons {
head: 10,
tail: null,
} },
} },
} }
// Create list2 by sharing the tail of list1
let list2 = Rc { Cons {
head: 20,
tail: Rc.clone(&list1),
} }
// Create list3 by sharing the tail of list1
let list3 = Rc { Cons {
head: 30,
tail: Rc.clone(&list1),
} }
// Now list1, list2, and list3 share the same tail data
println!("list1 reference count: \(Rc.strongCount(&list1))") // 3
}
Rc<T> Does NOT Enable Mutation
An important limitation of Rc<T> is that it gives you immutable references to the data inside. You cannot mutate data inside an Rc<T>:
let rc = Rc { vec![1, 2, 3] }
// rc.push(4) // Error: cannot mutate through an Rc
This is by design. Since multiple parts of your code might be referencing the same data, allowing mutation could cause data races and undefined behavior.
If you need interior mutability alongside reference counting, combine Rc<T> with RefCell<T>, which we'll explore in the next section.
Rc<T> vs Other Ownership Models
| Ownership | Cost | When to Use |
|---|---|---|
| Owned value | None | Single owner, known at compile time |
&T reference | None | Temporary borrowing |
Box<T> | Minimal | Single owner on heap |
Rc<T> | Reference count overhead | Multiple owners (single-threaded) |
Arc<T> | Atomic reference count | Multiple owners (multi-threaded) |
Reference Counting Performance
Rc<T> has a small but real performance cost:
- Memory overhead: Each
Rcallocates extra space for the reference count - CPU overhead: Cloning increments a counter; dropping decrements it (atomic operations in
Arc<T>) - Pointer indirection: Accessing the data requires dereferencing the pointer
For most applications, this overhead is negligible. However, in performance-critical code or when creating millions of references, consider whether you really need Rc<T> or if another approach would be better.
Rc<T> is Single-Threaded
Rc<T> is designed for single-threaded programs. If you need reference counting in a multi-threaded program, use Arc<T> (Atomic Reference Counted) instead, which uses atomic operations for thread safety.
// Single-threaded (use Rc)
let rc = Rc { data }
// Multi-threaded (use Arc)
let arc = Arc { data }
Real-World Example: Document Structure
Here's a practical example of using Rc<T> for a document structure where paragraphs might share common styling:
public struct Paragraph {
public text: String,
public style: Rc<Style>,
}
public struct Style {
public fontSize: Int,
public fontColor: String,
}
fn main() {
// Create a style that will be shared
let headingStyle = Rc { Style {
fontSize: 24,
fontColor: "blue",
} }
let heading = Paragraph {
text: "Welcome",
style: Rc.clone(&headingStyle),
}
let subheading = Paragraph {
text: "Introduction",
style: Rc.clone(&headingStyle),
}
// Both paragraphs share the same style object
println!("Style reference count: \(Rc.strongCount(&headingStyle))") // 3
}
Summary
Rc<T> enables multiple ownership of the same value through reference counting:
- Use
Rc<T>when you need multiple parts of your program to own the same data - Clone an
Rc<T>withRc.clone()(cheap—increments a counter) - The data is only dropped when the reference count reaches zero
Rc<T>provides immutable access; combine withRefCell<T>for interior mutability- Use
Arc<T>for multi-threaded code
In the next section, we'll see how to achieve interior mutability—the ability to mutate data through an immutable reference—using RefCell<T>.