<< Home

Chapter 10: Smart Pointers and Memory

Rust provides smart pointer types that encode ownership and borrowing rules in their APIs. These types help you place data on the heap, share ownership across parts of your program, mutate through shared references when you must, manage destruction of resources, and handle special layouts that pin addresses. This chapter covers Box, Rc and Arc, Cell and RefCell, the Drop trait, and Pin.

Box for heap allocation

Box<T> stores a value on the heap and gives ownership of a pointer to that value. Moving the Box moves the pointer value; the allocation remains in place. Use a box when you need a known size for a recursive type, when you want to transfer ownership cheaply, or when you need to allocate a large object off the stack.

Recursive enums with Box

Directly embedding a variant that contains itself would make the type size infinite; a box breaks the cycle by storing the child on the heap.

enum List {
  Nil,
  Cons(i32, Box<List>),
}

fn len(mut l: &List) -> usize {
  let mut n = 0;
  loop {
    match l {
      List::Nil => break,
      List::Cons(_, next) => { n += 1; l = next; }
    }
  }
  n
}
💡 Box gives unique ownership. If you need shared ownership, pick Rc or Arc instead.

Rc and Arc for shared ownership

Rc<T> is a single thread reference counting pointer; Arc<T> is atomic for multi thread sharing. Cloning either increases the strong count; when the last strong owner is dropped, the inner value is dropped. These types provide shared ownership; interior mutability is a separate choice.

Building shared graphs

Graphs and trees with parent links often require shared owners. Pair Rc with non owning Weak pointers to break cycles.

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
  value: i32,
  parent: RefCell<Weak<Node>>,
  children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
  let root = Rc::new(Node { value: 1, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
  let leaf = Rc::new(Node { value: 2, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
  *leaf.parent.borrow_mut() = Rc::downgrade(&root);
  root.children.borrow_mut().push(leaf);
}
⚠️ Reference cycles with only strong owners leak memory because counts never reach zero. Use Weak for back pointers to avoid cycles.

Sharing across threads with Arc

Arc is safe to clone and move between threads. Combine it with Mutex or RwLock when shared mutation is required.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
  let counter = Arc::new(Mutex::new(0_u64));
  let handles: Vec<_> = (0..4).map(|_| {
    let c = Arc::clone(&counter);
    thread::spawn(move || {
      for _ in 0..1_000 { *c.lock().unwrap() += 1; }
    })
  }).collect();

  for h in handles { h.join().unwrap(); }
  assert_eq!(*counter.lock().unwrap(), 4_000);
}

Cell and RefCell interior mutability

Interior mutability lets you mutate through a shared reference, with safety enforced at runtime. Cell<T> is for copy types and swap like operations; RefCell<T> performs dynamic borrow checking and works with any type. This pattern is useful when an API exposes shared references but needs to adjust hidden state.

Cell for small copies

Cell gives get and set for Copy types. It avoids borrow tracking by moving values in and out.

use std::cell::Cell;

struct Flag {
  hits: Cell<u32>,
}

impl Flag {
  fn hit(&self) { self.hits.set(self.hits.get() + 1); }
}

RefCell for dynamic borrows

RefCell enforces the borrowing rules at runtime. You borrow immutably with borrow and mutably with borrow_mut; violations panic, which means you should keep access patterns simple and well documented.

use std::cell::RefCell;

struct Log {
  lines: RefCell<Vec<String>>,
}

impl Log {
  fn push(&self, s: String) { self.lines.borrow_mut().push(s); }
  fn len(&self) -> usize { self.lines.borrow().len() }
}
💡 Pairing Rc<T> with RefCell<U> is a common pattern when multiple owners need mutation; for threads, use Arc with Mutex or RwLock.

Drop and resource management

Implement Drop to run code when a value goes out of scope. This is Rust’s approach to deterministic resource cleanup; it works for files, sockets, and any other external resource. Keep drop lightweight and avoid panicking inside it.

Custom cleanup with Drop

This wrapper writes a log line when the guard is destroyed. The pattern composes cleanly with early returns and errors.

struct Guard(&'static str);

impl Drop for Guard {
  fn drop(&mut self) {
    eprintln!("dropping {}", self.0);
  }
}

fn main() {
  let _g = Guard("temporary");
  // when main ends, drop runs automatically
}

Early release with std::mem::drop

Call std::mem::drop to release a value before the end of scope. This is useful when you need to free a lock or close a file so that later code can proceed.

use std::sync::{Mutex, Arc};
fn main() {
  let m = Arc::new(Mutex::new(0));
  let guard = m.lock().unwrap();
  // do work
  drop(guard); // releases the lock here
  // more work that does not hold the lock
}

Pin and self-referential data

Pin<P> guarantees that the pointee will not be moved again, which is necessary for types that store self references or for stackless futures that assume stable addresses. Most safe Rust code does not need Pin; use it when an API requires it, or when you build types that must never move once constructed.

Pinning indirection

Pinned values are usually behind a pointer, such as Pin<Box<T>> or Pin<Arc<T>>. You obtain a pinned pointer, then use projection methods to access fields safely.

use std::pin::Pin;

struct NeverMove {
  id: usize,
  // self-referential fields would go here …
}

fn main() {
  let x = Box::new(NeverMove { id: 7 });
  let pinned: Pin<Box<NeverMove>> = Box::pin(x);
  // pinned can be used safely; moving the box value would violate pinning
  assert_eq!(pinned.id, 7);
}

Pin in async code

Futures generated by async may hold pointers into their own stack frames. Executors pin futures before polling to keep addresses stable while state machines advance. When you write manual futures or project pinned fields, rely on pin-project or similar helpers to keep code correct.

⚠️ Self referential structs are hard to implement safely. Prefer designs that avoid internal self references; use indices or handles into containers instead.