Smart Pointers in Rust - Reference Counting

Rust’s ownership model, which ensures that every value has a single owner, is a powerful feature for memory safety. However, there are situations where a single value needs to have multiple owners. For example, in a graph data structure, multiple edges might point to the same node, and that node should only be cleaned up when the last edge pointing to it is removed.

For these scenarios, Rust provides the reference-counted smart pointer, Rc<T>.

What is Rc<T>?

Rc<T> (short for Reference Counted) is a smart pointer that enables multiple ownership of a value. It keeps track of the number of references—or “owners”—to a value allocated on the heap. The value is only deallocated when the last reference to it goes out of scope, meaning its reference count drops to zero.

Important: Rc<T> is designed for use in single-threaded contexts only. It does not use atomic operations for its reference counting, which makes it faster but not safe to share across threads.

Using Rc<T> for Shared Ownership

Let’s see how Rc<T> works with a classic Cons list example. Imagine we have a list a, and we want to create two new lists, b and c, that both share the tail of list a.

With Box<T>, this would be impossible because Box<T> enforces single ownership.

use std::rc::Rc;

// A Cons list that can have multiple owners.
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    // Create a list `a` holding (5 -> 10 -> Nil)
    // The list is wrapped in Rc to allow sharing.
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("Count after creating a = {}", Rc::strong_count(&a)); // Count is 1

    // Create list `b` that shares ownership of `a`.
    // Rc::clone only increments the reference count, it doesn't deep copy the data.
    let b = Cons(3, Rc::clone(&a));
    println!("Count after creating b = {}", Rc::strong_count(&a)); // Count is 2

    {
        // Create list `c` in a new scope that also shares `a`.
        let c = Cons(4, Rc::clone(&a));
        println!("Count after creating c = {}", Rc::strong_count(&a)); // Count is 3
    } // `c` goes out of scope, and the reference count decrements.

    println!("Count after c goes out of scope = {}", Rc::strong_count(&a)); // Count is 2
}

In this example:

  1. We create a and wrap it in Rc::new(). The initial reference count (or “strong count”) is 1.
  2. When we create b, we call Rc::clone(&a). This doesn’t perform a deep copy of the list data. Instead, it just increments the reference count on a and gives b a pointer to it. The count becomes 2.
  3. The same happens when we create c. The count becomes 3.
  4. When c goes out of scope at the end of the inner block, its reference is dropped, and the count decrements to 2.
  5. When main finishes, b and then a are dropped, the count becomes 0, and the heap data for the list is finally cleaned up.

Interior Mutability with Rc<T> and RefCell<T>

By default, the value inside an Rc<T> is immutable. You can’t get a mutable reference to it. If you need to have multiple owners and be able to mutate the data, you must use the interior mutability pattern, typically by combining Rc<T> with RefCell<T>.

RefCell<T> enforces Rust’s borrowing rules at runtime instead of compile time. A common pattern is Rc<RefCell<T>>, which gives you a value that can be shared among multiple owners and mutated by any of them (safely).

Thread-Safe Reference Counting: Arc<T>

As mentioned, Rc<T> is not thread-safe. If you try to use it in a multi-threaded context, the compiler will stop you. For thread-safe multiple ownership, Rust provides Arc<T> (Atomically Reference Counted).

Arc<T> works just like Rc<T>, but it uses atomic operations to manage the reference count. This ensures that the count is updated correctly even when multiple threads are accessing it simultaneously. This atomic access comes with a small performance cost compared to Rc<T>.

The thread-safe equivalent of Rc<RefCell<T>> is Arc<Mutex<T>>, which allows for shared ownership and safe mutation across threads.

Conclusion

Reference counting provides a practical way to manage memory for values that have multiple owners.

  • Use Rc<T> for single-threaded scenarios where you need shared ownership.
  • Use Arc<T> for multi-threaded scenarios to achieve the same goal safely.

These smart pointers are indispensable tools for handling more complex ownership requirements that go beyond the simple, single-owner model.