std: Pointer Types

CIS 198 Lecture 6

Reference: TRPL 5.8

&T and &mut T

  • Your basic, economy-class references.
  • Zero runtime cost; all checks are done at compile time.
  • Not allowed to outlive their associated lifetime.
  • Can introduce serious lifetime complexity if you’re not careful!
  • Use these unless you actually need something more complicated.

Box<T>

  • A Box<T> is one of Rust’s ways of allocating data on the heap.
  • A Box<T> owns a T, so its pointer is unique - it can’t be aliased (only referenced).
  • Boxes are automatically freed when they go out of scope.
  • Almost the same as unboxed values, but dynamically allocated.
  • Create a Box with Box::new().
1. let boxed_five = Box::new(5);

Box<T>

  • Pros:
  • Easiest way to put something on the heap.
  • Zero-cost abstraction for dynamic allocation.
  • Shares typical borrowing and move semantics.
  • Automatic destruction.
  • Cons (ish):
  • The T is strictly owned by the Box - only one owner.
    • This means the particular variable holding the box can’t go away until all references are gone - sometimes this won’t work out!

Aside: Box Syntax & Patterns

  • In homework 2 & 3, you may have noticed patterns like this:
1. let opt_box: Option<Box<i32>> = Some(Box::new(5));

3. match opt_box {
4. Some(boxed) => {
5. let unboxed = *boxed;
6. println!("Some {}", unboxed);
7. }
8. None => println!("None :("),
9. }

Aside: Box Syntax & Patterns

  • It’s currently not possible to destructure the Box inside the Option. :(
  • In Nightly Rust, it is, thanks to box syntax!
1. #![feature(box_syntax, box_patterns)]

3. let opt_box = Some(box 5);

5. match opt_box {
6. Some(box unboxed) => println!("Some {}", unboxed),
7. None => println!("None :("),
8. }
  • This may change before it reaches Stable.

std::rc::Rc<T>

  • Want to share a pointer with your friends? Use an Rc<T>!
  • A “Reference Counted” pointer.
  • Keeps track of how many aliases exist for the pointer.
  • Call clone() on an Rc to get a reference.
  • Increments its reference count.
  • No data gets copied!
  • When the ref count drops to 0, the value is freed.
  • The T can only be mutated when the reference count is 1 😕.
  • Same as the borrowing rules - there must be only one owner.
1. let mut shared = Rc::new(6);
2. {
3. println!("{:?}", Rc::get_mut(&mut shared)); // ==> Some(6)
4. }
5. let mut cloned = shared.clone(); // ==> Another reference to same data
6. {
7. println!("{:?}", Rc::get_mut(&mut shared)); // ==> None
8. println!("{:?}", Rc::get_mut(&mut cloned)); // ==> None
9. }

std::rc::Weak<T>

  • Reference counting has weaknesses: if a cycle is created:
  • A has an Rc to B, B has an Rc to A - both have count = 1.
  • They’ll never be freed! ~~Eternal imprisonment~~ a memory leak!
  • This can be avoided with weak references.
  • These don’t increment the strong reference count.
  • But that means they aren’t always valid!
  • An Rc can be downgraded into a Weak using Rc::downgrade().
  • To access it, turn it back into Rc: weak.upgrade() -> Option<Rc<T>>
  • Nothing else can be done with Weak - upgrading prevents the value from becoming invalid mid-use.

Strong Pointers vs. Weak Pointers

  • When do you use an Rc vs. a Weak?
  • Generally, you probably want a strong reference via Rc.
  • If your ownership semantics need to convey a notion of possible access to data but no ownership, you might want to use a Weak.
  • Such a structure would also need to be okay with the Weak coming up as None when upgraded.
  • Any structure with reference cycles may also need Weak, to avoid the leak.
  • Note: Rc cycles are difficult to create in Rust, because of mutability rules.

std::rc::Rc<T>

  • Pros:
  • Allows sharing ownership of data.
  • Cons:
  • Has a (small) runtime cost.
    • Holds two reference counts (strong and weak).
    • Must update and check reference counts dynamically.
  • Reference cycles can leak memory. This can only be resolved by:
    • Avoiding creating dangling cycles.
    • Garbage collection (which Rust doesn’t have).

Cells

  • A way to wrap data to allow interior mutability.
  • An immutable reference allows modifying the contained value!
  • There are two types of cell: Cell<T> and RefCell<T>.
1. struct Foo {
2. x: Cell<i32>,
3. y: RefCell<u32>,
4. }

std::cell::Cell<T>

  • A wrapper type providing interior mutability for Copy types.
  • Cell<T>s cannot contain references.
  • Get values from a Cell with get().
  • Update the value inside a Cell with set().
  • Can’t mutate the T, only replace it.
  • Generally pretty limited, but safe & cheap.
1. let c = Cell::new(10);
2. c.set(20);
3. println!("{}", c.get()); // 20

std::cell::Cell<T>

  • Pros:
  • Interior mutability.
  • No runtime cost!
  • Small allocation cost.
  • Cons:
  • Very limited - only works on Copy types.

std::cell::RefCell<T>

  • A wrapper type providing interior mutability for any type.
  • Uses dynamic borrow checking rules (performed at runtime).
  • This may cause a panic at runtime.
  • Borrow inner data via borrow() or borrow_mut().
  • These may panic if the RefCell is already borrowed!
1. use std::cell::RefCell;

3. let refc = RefCell::new(vec![12]);
4. let mut inner = refc.borrow_mut();
5. inner.push(24);
6. println!("{:?}", *inner); // [12, 24]

8. let inner2 = refc.borrow();
9. // ==> Panics since refc is already borrow_mut'd!

std::cell::RefCell<T>

  • A common paradigm is putting a RefCell inside an Rc to allow shared mutability.
  • Not thread-safe! borrow() et al don’t prevent race conditions.
  • There is no way (in stable Rust) to check if a borrow will panic before executing it.
  • borrow_state(&self) is an unstable way to do this.

std::cell::RefCell<T>

  • Pros:
  • Interior mutability for any type.
  • Cons:
  • Stores an additional borrow state variable.
  • Must check borrow state to dynamically allow borrows.
  • May panic at runtime.
  • Not thread-safe.

std::cell::Ref<T> & RefMut<T>

  • When you invoke borrow() on a RefCell<T>, you actually get Ref<T>, not &T.
  • Similarly, borrow_mut() gives you a RefMut<T>.
  • These are pretty simple wrapper over &T, but define some extra methods.
  • Sadly, all of them are unstable pending the cell_extras feature 😞.

*const T & *mut T

  • C-like raw pointers: they just point… somewhere in memory.
  • No ownership rules.
  • No lifetime rules.
  • Zero-cost abstraction… because there is no abstraction.
  • Requires unsafe to be dereferenced.
  • May eat your laundry if you’re not careful.
  • Use these if you’re building a low-level structure like Vec<T>, but not in typical code.
  • Can be useful for manually avoiding runtime costs.
  • We won’t get to unsafe Rust for a while, but for now:
  • Unsafe Rust is basically C with Rust syntax.
  • Unsafe means having to manually maintain Rust’s assumptions (borrowing, non-nullability, non-undefined memory, etc.)