15. Smart Pointers

Introduction

Learning objectives

  • Understand what a smart pointer is
  • Identify common smart pointers
  • Identify use-cases of smart pointers

Smart pointer

Smart pointers … are data structures that act like a pointer but also have additional metadata and capabilities.

Common smart pointers

  • Box<T>
  • Rc<T>
  • Ref<T>
    • RefMut<T>
    • RefCell<T>

Box<T>

Box<T>

Points to data on the heap

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

Box<T> Usage

  • Size of T unkown & need to use value of T in context where size is needed
  • Own a value & only care that T implements a trait, not specific type of T
  • Large amount of data & need to transfer ownership without copying
    • Transfering ownership moves data around stack (slow)
    • Instead, store data on heap and only keep pointers on stack (fast)

Recursive types with Boxes

(1, (2, (3, Nil)))

cons list

Each element contains

  • a value
  • a cons list

Recursive types with Boxes

enum List {
  Cons(i32, List),
  Nil,
}

Recursive types with Boxes

Cons list as nested boxes leading to infinity

Recursive types with Boxes

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

Recursive types with Boxes

Cons list as nested boxes with size usize

Deref & Drop

Deref trait

Customize behavior of dereference * operator

Code agnostic to pointer type

A pointer ‘points’ to a value

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Box<T> as a pointer

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

OurBox

struct OurBox<T>(T);

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

OurBox

fn main() {
    let x = 5;
    let y = OurBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

OurBox

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

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

Deref coercion

If T implements Deref, then &T can be converted to &U.

Deref coercion

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

fn main() {
    let m = OurBox::new(String::from("Rust"));
    // hello(&(*m)[..]); w/o Deref coercion
    hello(&m);
}

Drop trait

Customize cleanup behavior when value goes out of scope

Drop trait

impl<T> Drop for OurBox<T> {
    fn drop(&mut self) {
        println!("Dropping OurBox");
    }
}

fn main() {
    let _b = OurBox::new(5);
    println!("OurBox '_b' created");
}

std::mem::drop

struct OurBox<T>(T);

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

fn main() {
    let b = OurBox::new(5);
    println!("OurBox 'b' created");
    drop(b);
    println!("OurBox 'b' dropped")
}

Rc<T>

Rc<T>

  • Reference counting pointer
    • multiple owners
    • count how many references a value has
  • Use when
    • need access to data in multiple places
    • don’t know at compile time who uses data last
  • Used in R for modify-in-place vs. copy-on-modify

Sharing data with Rc<T>

Cons lists 'a' is contained within cons lists 'b' and 'c'.

Sharing data with Rc<T>

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

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

Sharing data with Rc<T>

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

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Tracking the count of an Rc<T>

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

RefCell<T>

RefCell<T>

  • “A mutable memory location with dynamically checked borrow rules”
  • Borrowing rules enforced at runtime instead of compile time
  • Use to implement the interior mutability pattern
    • mutate data via immutable references

Interior mutability with RefCell<T> - mock objects

  • Create a library to track a value against a maximum value & sends message

  • Create a library to track a value relative to a maximum value

    • Example: API with limited calls per day
  • Send message with status of tracked value

    • User expected to send message via Messenger trait
  • Use a mock object for testing

Interior mutability with RefCell<T> - mock objects

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

pub trait Messenger {
    fn send(&self, msg: &str);
}

Interior mutability with RefCell<T> - mock objects

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

Interior mutability with RefCell<T> - mock objects

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

Combining RefCell<T> & Rc<T>

RefCell<T> + Rc<T> = mutable data with multiple owners

Combining RefCell<T> & Rc<T>

Cons lists 'a' is contained within cons lists 'b' and 'c'.
  • Rc<T>
    • multiple owners of a
    • lists are immutable
  • Rc<T> + RefCell<T>
    • multiple owners
    • mutable lists

Combining RefCell<T> & Rc<T>

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {a:?}");
    println!("b after = {b:?}");
    println!("c after = {c:?}");
}

RefCell<T> & Rc<T> - memory leaks

Cons lists 'a' and 'b' have references to each other, causing a memory leak.

Prevent reference cycles

  • Replace Rc<T> with Weak<T>
  • Weak<T>
    • weak reference
    • no ownership relationship
    • count does not affect Rc<T> clean up

Weak<T>

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

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

Weak<T>

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

    // ...
}

Review

When to use Box<T>, Rc<T>, and RefCell<T>

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