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 keeps track of the number of references to data on the heap so that data can have multiple owners.

fn main() {
    todo!();
}

RefCell

std

The RefCell<T> type with its interior mutability gives us a type that we can use when we need an immutable type but need to change an inner value of that type; it also enforces the borrowing rules at runtime instead of at compile time.

use std::cell::RefCell;

// The `RefCell` type in Rust is used for _interior mutability_,
// a pattern that allows you to mutate data even when there are immutable
// references to it. `RefCell` dynamically borrow checks at runtime,
// unlike Rust's standard borrowing rules (checks at compile time).
// Attempts to violate borrowing rules (like having multiple mutable borrows)
// will cause a panic at runtime. `RefCell` is single-threaded.
// The corresponding `Sync` version of `RefCell<T>` is `RwLock<T>`.
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);
    } // The immutable borrow ends here

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

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