Lesson 20 of 22·18 min

Smart Pointers

A reference, &T, is a pointer: an address of some data, plus the borrow checker's guarantee that the data is still there. A smart pointer is a struct that also points at data but adds something on top: it owns the data, counts how many owners there are, or checks borrows at runtime. You have used two already. String and Vec<T> own a buffer on the heap and free it when they are dropped.

This lesson covers the smart pointers in the standard library you will meet first: Box<T> to put a value on the heap, the Deref and Drop traits that make smart pointers work, Rc<T> for data with several owners, and Cell<T> and RefCell<T> for changing data behind a shared reference.

Box<T>: a value on the heap

Box::new(value) moves the value to the heap and gives you back a box that owns it. The box itself is just a pointer, and you use it much like the value:

fn main() {
    let boxed = Box::new(1_000_u64);
    let doubled = *boxed * 2;
 
    println!("{boxed} {doubled}");
}
1000 2000

*boxed dereferences the box to reach the u64, the same * as for references. println! and method calls look through the box on their own. When boxed goes out of scope, the box frees the heap memory along with the value in it.

A box is always the size of a pointer, however large its contents are:

fn main() {
    let pixels = Box::new([0_u8; 1_000_000]);
 
    println!("the box: {} bytes", size_of_val(&pixels));
    println!("the array: {} bytes", size_of_val(&*pixels));
}
the box: 8 bytes
the array: 1000000 bytes

(8 bytes on a 64-bit machine.) Moving pixels into a function or a struct copies 8 bytes, not a million.

Most values don't need a box. The stack is faster and Vec or String already put their contents on the heap. You reach for Box in three situations:

  • A recursive type, whose size would otherwise be infinite.
  • A trait object, Box<dyn Trait>, from the trait objects lesson: a value whose concrete type, and so size, is only known at runtime.
  • A large value you want to move around cheaply.

Recursive types

An arithmetic expression like (2 + 3) * 4 is a tree: an addition or a multiplication has two smaller expressions inside it. As an enum, a variant contains the enum itself. Written directly, that type would need infinite space, since an Expr would contain an Expr that contains an Expr...

A box breaks the cycle. An Add holds two pointers, whose size is known, and the inner expressions live on the heap:

enum Expr {
    Num(f64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}
 
fn eval(expr: &Expr) -> f64 {
    match expr {
        Expr::Num(n) => *n,
        Expr::Add(left, right) => eval(left) + eval(right),
        Expr::Mul(left, right) => eval(left) * eval(right),
    }
}
 
fn main() {
    // (2 + 3) * 4
    let sum = Expr::Add(Box::new(Expr::Num(2.0)), Box::new(Expr::Num(3.0)));
    let expr = Expr::Mul(Box::new(sum), Box::new(Expr::Num(4.0)));
 
    println!("{}", eval(&expr));
}
20

In the match, left and right are &Box<Expr>, and they are passed to eval, which takes &Expr. That works thanks to deref coercion, next.

A box can also be moved out of with *, which gives the value back and frees the box: let inner: Expr = *boxed;. That is special to Box; other smart pointers only let you borrow what they point at.

Deref: acting like a reference

What makes * work on a box is the Deref trait. It has one method, deref, which returns a reference to the inner value, and an associated type, Target, for the type of that value. *boxed is really *(boxed.deref()).

You can implement it on your own wrapper types:

use std::ops::Deref;
 
struct Username(String);
 
impl Deref for Username {
    type Target = str;
 
    fn deref(&self) -> &str {
        &self.0
    }
}
 
fn greet(name: &str) {
    println!("Hello, {name}!");
}
 
fn main() {
    let user = Username(String::from("ferris"));
 
    println!("{} characters, {}", user.len(), user.to_uppercase());
    greet(&user);
}
6 characters, FERRIS
Hello, ferris!

Two things happen here:

  • Method calls look through Deref. user.len() finds no len on Username, so the compiler tries str, the Target, and finds it there.
  • Deref coercion. greet takes a &str and gets a &Username. The compiler inserts deref calls until the types match: &Username becomes &str. The same rule turns &String into &str, &Vec<T> into &[T] and &Box<Expr> into &Expr, and it can apply several times in a row.

There is a DerefMut trait for the &mut version. Implement Deref for types that wrap and act like the thing they point at, as smart pointers and newtypes do. Don't use it to borrow another type's methods for an unrelated type; that makes code hard to follow.

Drop: cleaning up

When a value goes out of scope, Rust drops it. For a type that implements Drop, that first runs its drop method, which is how Box, Vec and String free their memory, a file closes itself, and a lock gets released. You can implement it too:

struct Lamp {
    room: &'static str,
}
 
impl Drop for Lamp {
    fn drop(&mut self) {
        println!("turning off the {} lamp", self.room);
    }
}
 
fn main() {
    let _kitchen = Lamp { room: "kitchen" };
    let _hall = Lamp { room: "hall" };
    {
        let _porch = Lamp { room: "porch" };
        println!("leaving the inner block");
    }
    println!("end of main");
}
leaving the inner block
turning off the porch lamp
end of main
turning off the hall lamp
turning off the kitchen lamp

Values are dropped at the end of their scope, in the reverse order they were declared: _hall was declared after _kitchen, so it goes first. A struct's fields are dropped after its own drop runs, in declaration order.

The variables start with _ only so the compiler doesn't warn that they are never used. Be careful with a bare _, though: let _ = Lamp { .. }; doesn't bind the value at all, so it is dropped right away on that line.

To drop something before the end of its scope, pass it to drop, a function from the prelude that takes ownership and does nothing with it:

struct Lamp {
    room: &'static str,
}
 
impl Drop for Lamp {
    fn drop(&mut self) {
        println!("turning off the {} lamp", self.room);
    }
}
 
fn main() {
    let kitchen = Lamp { room: "kitchen" };
    drop(kitchen);
    println!("after drop");
}
turning off the kitchen lamp
after drop

Since kitchen was moved into drop, the borrow checker stops you from using it afterwards, and its drop method can't run twice. Moving a value into any function works the same way: it is dropped when that function ends, unless the function passes it on.

This pattern, tying a resource to a value so it is released when the value goes away, is called RAII. It is why Rust has no close() or free() calls to forget.

Rc<T>: several owners

Ownership normally has one owner per value. Sometimes data genuinely belongs to several parts of a program, and none of them outlives the others in an obvious way: a song that is on several playlists, a node in a graph with several edges into it.

Rc<T> (reference counted) allows that. Rc::new puts the value on the heap next to a counter. Rc::clone doesn't copy the value; it makes another pointer to it and adds one to the count. Dropping an Rc subtracts one, and when the count reaches zero, the value is dropped:

use std::rc::Rc;
 
fn main() {
    let theme = Rc::new(String::from("dark"));
    println!("count after new: {}", Rc::strong_count(&theme));
 
    {
        let editor = Rc::clone(&theme);
        let terminal = Rc::clone(&theme);
        let count = Rc::strong_count(&theme);
        println!("{editor} and {terminal}: count {count}");
    }
 
    println!("count after the block: {}", Rc::strong_count(&theme));
}
count after new: 1
dark and dark: count 3
count after the block: 1

Each of theme, editor and terminal is an owner. None of them is special; whichever is dropped last frees the String.

A shared value in several collections:

use std::rc::Rc;
 
struct Song {
    title: String,
    seconds: u32,
}
 
fn total_seconds(playlist: &[Rc<Song>]) -> u32 {
    playlist.iter().map(|song| song.seconds).sum()
}
 
fn main() {
    let intro = Rc::new(Song {
        title: String::from("Borrowed Time"),
        seconds: 185,
    });
    let outro = Rc::new(Song {
        title: String::from("Dangling Pointer"),
        seconds: 240,
    });
 
    let morning = vec![Rc::clone(&intro), Rc::clone(&outro)];
    let evening = vec![Rc::clone(&outro)];
 
    let (am, pm) = (total_seconds(&morning), total_seconds(&evening));
    println!("morning {am}s, evening {pm}s");
    println!("{}: {} owners", intro.title, Rc::strong_count(&intro));
    println!("{}: {} owners", outro.title, Rc::strong_count(&outro));
}
morning 425s, evening 240s
Borrowed Time: 2 owners
Dangling Pointer: 3 owners

Each song exists once in memory, however many playlists include it. Rc implements Deref, so song.seconds reaches through it.

intro.clone() would do the same as Rc::clone(&intro), but the convention is the longer form: it makes clear at a glance that this is a cheap pointer copy and not a deep clone of the data.

Three limits to know:

  • Read-only. An Rc only hands out shared references to its value, since other owners may be looking at it. Mutation needs RefCell, below.
  • Single thread. Updating the count isn't thread-safe, so the compiler won't let an Rc cross to another thread. The thread-safe version is Arc<T>, covered in the concurrency lesson.
  • Cycles leak. If two values hold Rcs to each other, neither count reaches zero and neither is ever dropped. For back-pointers, such as a child pointing at its parent, use Weak<T> from Rc::downgrade, a pointer that doesn't count as an owner. See the Rust Book for the details.

Interior mutability: Cell<T> and RefCell<T>

The borrow rules say a value is either shared (&T) or mutable (&mut T), never both, and the compiler checks this before the program runs. Sometimes that check is too strict: a method takes &self but wants to update a counter, or several Rc owners need to change a shared value.

Interior mutability types let you change their contents through a shared reference. They still enforce the rules, just in a different way.

Cell<T> is for small Copy values. You never borrow what's inside; you get a copy out and set a new value in, so no reference can be left dangling. RefCell<T> works for any type: borrow() returns a shared borrow and borrow_mut() a mutable one, and the "many readers or one writer" rule is checked at runtime instead of at compile time.

use std::cell::RefCell;
 
fn main() {
    let draft = RefCell::new(String::from("Hello"));
 
    draft.borrow_mut().push_str(", world");
 
    let text = draft.borrow();
    println!("{text} ({} bytes)", text.len());
}
Hello, world (12 bytes)

draft isn't declared mut, yet its String changed. borrow_mut() returns a guard that acts like a &mut String and releases the borrow when it is dropped, here at the end of that statement. borrow() returns a similar guard that acts like &String.

Both types are most useful inside a struct whose methods take &self:

use std::cell::{Cell, RefCell};
 
struct Spellchecker {
    known: Vec<&'static str>,
    lookups: Cell<u32>,
    unknown_seen: RefCell<Vec<String>>,
}
 
impl Spellchecker {
    fn check(&self, word: &str) -> bool {
        self.lookups.set(self.lookups.get() + 1);
 
        let known = self.known.contains(&word);
        if !known {
            self.unknown_seen.borrow_mut().push(word.to_string());
        }
        known
    }
}
 
fn main() {
    let checker = Spellchecker {
        known: vec!["borrow", "crate", "trait"],
        lookups: Cell::new(0),
        unknown_seen: RefCell::new(Vec::new()),
    };
 
    for word in ["crate", "borow", "trait", "lifetme"] {
        println!("{word}: {}", checker.check(word));
    }
 
    let unknown = checker.unknown_seen.borrow();
    println!("{} lookups, unknown: {unknown:?}", checker.lookups.get());
}
crate: true
borow: false
trait: true
lifetme: false
4 lookups, unknown: ["borow", "lifetme"]

To the caller, check looks like a read-only operation, so it takes &self. The statistics it keeps are an internal detail, stored in cells.

Rc<RefCell<T>>: shared and mutable

Put the two together and you get a value with several owners, any of which can change it. Here two remotes control the same thermostat:

use std::cell::RefCell;
use std::rc::Rc;
 
#[derive(Debug)]
struct Thermostat {
    target: f64,
    changes: u32,
}
 
struct Remote {
    name: &'static str,
    thermostat: Rc<RefCell<Thermostat>>,
}
 
impl Remote {
    fn set(&self, target: f64) {
        let mut thermostat = self.thermostat.borrow_mut();
        thermostat.target = target;
        thermostat.changes += 1;
        println!("{} set {target}°C", self.name);
    }
}
 
fn main() {
    let thermostat = Rc::new(RefCell::new(Thermostat {
        target: 20.0,
        changes: 0,
    }));
 
    let phone = Remote {
        name: "phone",
        thermostat: Rc::clone(&thermostat),
    };
    let wall = Remote {
        name: "wall panel",
        thermostat: Rc::clone(&thermostat),
    };
 
    phone.set(22.5);
    wall.set(21.0);
 
    println!("{:?}", thermostat.borrow());
}
phone set 22.5°C
wall panel set 21°C
Thermostat { target: 21.0, changes: 2 }

Rc provides the several owners, RefCell the mutation. In set, thermostat is the guard from borrow_mut(); it derefs to the Thermostat, so its fields can be assigned directly, and it releases the borrow at the end of the method.

This combination is powerful but moves errors from compile time to runtime, and it makes it harder to see who changes what. Prefer plain ownership and references where they fit, and reach for Rc<RefCell<T>> when the data really is shared.

Which one to use

TypeGives youChecked
Box<T>one owner, value on the heapcompile time
Rc<T>several owners, read-onlycompile time
Cell<T>get and set a Copy value through &Tno borrows
RefCell<T>borrow mutably through &Truntime
Rc<RefCell<T>>several owners that can all change itruntime
Arc<Mutex<T>>the same, across threadsruntime

Common mistakes

A recursive type without a box

enum Expr {
    Num(f64),
    Add(Expr, Expr),
}
 
fn main() {
    let expr = Expr::Num(1.0);
    if let Expr::Num(n) = expr {
        println!("{n}");
    }
}

The compiler can't give Expr a size, because it would have to include itself. Wrap the recursive fields in Box, as the help suggests: Add(Box<Expr>, Box<Expr>). A Vec<Expr> field would work as well, since a Vec also stores its items on the heap.

Calling drop as a method

struct Lamp {
    room: &'static str,
}
 
impl Drop for Lamp {
    fn drop(&mut self) {
        println!("turning off the {} lamp", self.room);
    }
}
 
fn main() {
    let mut kitchen = Lamp { room: "kitchen" };
    kitchen.drop();
    println!("after drop");
}

Rust calls Drop::drop itself when the value goes away. If you could call it too, it would run twice. Use the drop(kitchen) function instead, which takes ownership so the value can't be used afterwards.

Mutating through an Rc

use std::rc::Rc;
 
fn main() {
    let scores = Rc::new(vec![10, 20]);
    let shared = Rc::clone(&scores);
 
    shared.push(30);
 
    println!("{scores:?}");
}

Rc only implements Deref, not DerefMut: other owners might be reading the value. If the owners need to change it, store a RefCell in the Rc, Rc::new(RefCell::new(vec![10, 20])), and call borrow_mut().

Two overlapping RefCell borrows

use std::cell::RefCell;
 
fn main() {
    let draft = RefCell::new(String::from("Hello"));
 
    let reading = draft.borrow();
    draft.borrow_mut().push('!');
 
    println!("{reading}");
}

This compiles, because RefCell moved the check to runtime, and then panics: reading still holds a shared borrow when borrow_mut() asks for a mutable one. The same code with a plain String would have been rejected by the compiler. Keep guards short-lived (use draft.borrow() inline, or drop the guard first), or use try_borrow_mut(), which returns an Err instead of panicking.

Summary

  • A smart pointer owns or manages the data it points at. String and Vec are smart pointers too.
  • Box<T> puts a value on the heap with a single owner. Use it for recursive types, trait objects and large values.
  • Deref makes * and method calls work through a wrapper, and deref coercion turns &Box<T>, &String or &YourWrapper into the reference a function expects.
  • Drop runs cleanup when a value goes out of scope, in reverse declaration order. Drop something early with drop(value), never value.drop().
  • Rc<T> gives a value several read-only owners, counted at runtime. Rc::clone copies the pointer, not the data. It is single-threaded, and cycles of Rcs leak.
  • Cell<T> and RefCell<T> allow mutation through &T. RefCell checks the borrow rules at runtime and panics when they are broken.
  • Rc<RefCell<T>> is shared, mutable data. Use it when ownership really is shared, not as a way around the borrow checker.

Practise what you read

Challenges that use the ideas from this lesson.