Rust - Smart Pointers


Smart pointers in Rust are like smart pointers in C++. Unlike references which only borrow data, smart pointers can own the data they point to.

Box

A Box<T> stores data on the heap rather than the stack. A pointer to that heap data remains on the stack. They are useful when:

  • Don't know the size of the type at compile time but will need to know the exact size during run time
  • Want to transfer ownership of a large amount of data without copying.
  • When you own a value and only care about its implemented traits, not its type (polymorphism)

Immutable or mutable borrow checking is done at compile time.

Example:

pub enum LinkedList {
    Con(i32, Box<LinkedList>),
    Nil,
}


use crate::LinkedList::{Con, Nil};

fn main() {
    let list = Con(1,
        Box::new(Con(2, 
                Box::new(Con(3, Box::new(Nil))))));

    assert_eq!(1, match list {
        Con(value, next) => value,
        Nil => -1,
    });
}

Deref Coercion

Deref coercion allows a referenced type to another type, for example, &String can be converted to &str because String implements the Deref trait and returns &str.

The DerefMut trait can be used to override the * operator on mutable references.

In addition, mutable references can be coerced to become immutable if the Deref trait is implemented.

Example:

use std::ops::Deref;
use std::ops::DerefMut;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

fn deref_coercion(name: &str) {
    println!("{}", name);
}

fn deref_mut_coercion(num: &mut f32) {
    *num += 3.0
}


fn main() {
    let x = MyBox::new(2);
    assert_eq!(2, *x);

    let n = MyBox::new(String::from("Rust"));
    deref_coercion(&n);
    let mut m = MyBox::new(12.2);
    deref_mut_coercion(&mut m);
}

Refcell

RefCell<T> is like Box<T> except the borrow checker is checked at runtime instead of compile time. This allows the programmer to make code that they know will work, but the borrow checker is unsure about.

RefCell<T> is only used in single-threaded scenarios.

Both immutable and mutable borrow checking is done at runtime. The value inside of RefCell<T> can be mutated even if RefCell<T> is immutable.

This is useful when you need to mutate something that is passed in as immutable.

Reference Counted

A reference counted smart pointer is the same as a shared pointer in C++. The Rc<T> pointer keeps track of the number of references to it and frees up memory when the reference count is zero.

Rc<T> can only be used for single-threaded scenarios.

Only immutable borrow checking is done at compile time.

Rc<T> can only hold immutable data. Pair with RefCell<T> for mutability.

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let v = Rc::new(RefCell::new(vec![1, 2, 3]));
    let w = Rc::clone(&v);
    let x = Rc::clone(&v);

    // Add 4 to vector
    v.borrow_mut().push(4);

    //would_cause_panic(&v);

    println!("{:?}", x);
}

fn would_cause_panic(v: &Rc<RefCell<Vec<u32>>>) {
    let mut one_borrow = v.borrow_mut();
    // Panic because two mutable references
    // During rumtime with RefCell
    let mut two_borrow = v.borrow_mut();
}
Strong vs Weak Count

When strong_count reaches 0, the instance is cleaned up. Clean up does not care how many weak references there are. Using Weak<T> is one way to help prevent memory leaks (via circular referencing).

Since what Weak<T> references may have been dropped, it is necessary to call upgrade which returns an Option<RC<T>> to verify that the reference still exists.

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

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 6,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:#?}", leaf.parent.borrow().upgrade()); // None

    let branch = Rc::new(Node {
        value: 42,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    // Downgrade strong to weak
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:#?}", leaf.parent.borrow().upgrade()); // Some
}

Traits

Smart pointers implement the Deref and Drop traits.