Smart Pointers

RecipeCrates
Boxstd
Rcstd
RefCellstd
  • Rc<T> enables multiple owners of the same data; Box<T> and RefCell<T> have single owners.
  • Box<T> allows immutable or mutable borrows checked at compile time; Rc<T> allows only immutable borrows checked at compile time; RefCell<T> allows immutable or mutable borrows checked at runtime.
  • Because RefCell<T> allows mutable borrows checked at runtime, you can mutate the value inside the RefCell<T> even when the RefCell<T> is immutable.

Box

book-rust-box Rust by example - box std

All values in Rust are stack-allocated by default. Box<T> allow you to store data on the heap rather than the stack. What remains on the stack is the pointer to the heap data.

Boxes provide ownership for this allocation, and drop their contents when they go out of scope. Boxes also ensure that they never allocate more than isize::MAX bytes.

The Box<T> type is a smart pointer, because it implements the std::ops::Deref⮳ trait, which allows Box<T> values to be treated like a reference. You can use the dereference operator * or 'deref coercion' with the . operator to retrieve its inner value.

let boxed: Box<u8> = Box::new(1);
let _val: u8 = *boxed;
let boxed = Box::new("example");
// Deref coercion: equivalent to (*boxed.deref()).len()
let _val = boxed.len();

Use Box<T> when

  • you have a dynamically sized type, whose size can’t be known at compile time,
  • you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type,
  • you don't want to rely on stack space.

// Define a Node struct to represent a single element in a linked list.
struct Node {
    value: i32,
    // Node is a recursive data type.
    next: Option<Box<Node>>,
}

impl Node {
    fn new(value: i32) -> Self {
        Node { value, next: None }
    }

    // Recursively traverses the list until it finds the last node
    // (where next is None) and sets its next field to a new Node.
    fn append(&mut self, value: i32) {
        match self.next {
            Some(ref mut next_node) => next_node.append(value),
            None => self.next = Some(Box::new(Node::new(value))),
        }
    }

    fn print(&self) {
        print!("{}", self.value);
        if let Some(ref next_node) = self.next {
            print!(" -> ");
            next_node.print();
        } else {
            println!();
        }
    }
}

fn main() {
    // The linked list has an unknown number of nodes,
    // thus its size is not fixed.
    // It could not be stored directly on the stack.
    // By using `Box`, which pointer to the heap has a defined size,
    // we can create the `head` local variable on the stack.
    let mut head = Node::new(1);
    head.append(2);
    head.append(3);
    head.append(4);

    head.print(); // Output: 1 -> 2 -> 3 -> 4
}

Rc

std

The Rc<T> type (for "Reference Counted") enables shared ownership of a value.

  • Rc maintains a reference count of the number of owners. You can create additional references to the data using the clone method. Cloning an Rc only increments the reference count without duplicating the data. When the last owner goes out of scope, the data is automatically cleaned up (dropped).
  • If you need mutability, put a Cell or RefCell inside the Rc.
  • Rc automatically dereferences to T (via the Deref trait), so you can call T’s methods on a value of type Rc<T>.
  • Rc is commonly used in data structures, such as graphs and linked lists, where multiple nodes might need to share ownership of certain nodes or data.

Keep in mind that Rc is not thread-safe. For concurrent scenarios, you should use Arc (Atomic Reference Counted), which provides similar functionality with thread safety.

use std::rc::Rc;

fn main() {
    let a = Rc::new(vec![1.0, 2.0, 3.0]);
    // The two syntaxes below are equivalent.
    let b = a.clone();
    let c = Rc::clone(&a);
    // `b` and `c` both point to the same memory location as `a`.

    // Gets the number of (Rc) pointers to this allocation.
    assert_eq!(3, Rc::strong_count(&a));
    // Dropping one of the pointers decrements the strong reference count.
    drop(c);
    assert_eq!(2, Rc::strong_count(&a));

    // `Rc` is a smart pointer, so we can dereference it.
    println!("{:?}", *a);
    b.iter().for_each(|x| print!("{} ", x));

    // We can get a mutable reference to the inner value,
    // if there are no other `Rc` or `Weak` pointers to the same allocation.
    // Returns `None` ottherwise.
    let mut y = Rc::new(4);
    *Rc::get_mut(&mut y).unwrap() = 5;
    assert_eq!(*y, 5);

    // We can also consume the `Rc` to return its inner value,
    // if the Rc has exactly one strong reference.
    // See also `Rc::into_inner`.
    let x = Rc::new(6);
    assert_eq!(Rc::try_unwrap(x), Ok(6));
}

Also of note: the Weak type, typically obtained via Rc::downgrade, allows for non-owning (weak) references to the data. This can help prevent reference cycles that could lead to memory leaks.

RefCell

std

Rust memory safety allows (i) several immutable references (&T) to an object T; or (ii) one mutable reference (&mut T). This is enforced at compile time. However, sometimes it is required to have multiple references to an object and yet mutate it. RefCell<T> (and related types Cell<T> and OnceCell<T>) have interior mutability, a pattern that allows you to mutate data even when there are immutable references to it.

These types are used in scenarios involving shared state within a single thread, like GUI applications, or when creating complex data structures like graphs.

  • RefCell keeps track of borrowing rules at runtime and ensures that only one mutable or multiple immutable borrows exist at a time. Attempts to violate borrowing rules (like having multiple mutable borrows) will cause a panic at runtime. Common methods include borrow, borrow_mut, and try_borrow.
use std::cell::RefCell;

fn main() {
    // Create a RefCell containing a vector of integers
    let data = RefCell::new(vec![1, 2, 3, 4, 5]);

    // Borrow the data immutably
    {
        let borrowed_data = data.borrow();
        println!("Borrowed (immutable): {:?}", borrowed_data);

        // We can borrow the data immutably again
        let _borrowed_data2 = data.borrow();
    } // The immutable borrow ends here (when the `Ref` guard returned by `borrow` exits scope.)

    // Borrow the data mutably and modify it
    {
        let mut borrowed_data = data.borrow_mut();
        borrowed_data.push(6);
        println!("Borrowed (mutable): {:?}", borrowed_data);

        // We can't borrow the data again while it's borrowed mutably
        assert!(data.try_borrow().is_err());
        assert!(data.try_borrow_mut().is_err());
    } // The mutable borrow ends here

    // Borrow the data immutably again to check the modification
    {
        if let Ok(borrowed_data) = data.try_borrow() {
            println!("Borrowed (immutable again): {:?}", borrowed_data);
        }
    }

    // We can also consume the `RefCell`, returning the wrapped value.
    let _data = data.into_inner();
}
  • RefCell<T> (and Cell<T>, OnceCell<T>) do not implement Sync and are therefore single-threaded. The corresponding Sync version of RefCell<T> is RwLock<T>. Use Mutex<T>, RwLock<T>, OnceLock<T>, or atomic types when working with multiple threads.