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));
}20In 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 nolenonUsername, so the compiler triesstr, theTarget, and finds it there. - Deref coercion.
greettakes a&strand gets a&Username. The compiler insertsderefcalls until the types match:&Usernamebecomes&str. The same rule turns&Stringinto&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 lampValues 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 dropSince 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: 1Each 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 ownersEach 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
Rconly hands out shared references to its value, since other owners may be looking at it. Mutation needsRefCell, below. - Single thread. Updating the count isn't thread-safe, so the compiler
won't let an
Rccross to another thread. The thread-safe version isArc<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, useWeak<T>fromRc::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
| Type | Gives you | Checked |
|---|---|---|
Box<T> | one owner, value on the heap | compile time |
Rc<T> | several owners, read-only | compile time |
Cell<T> | get and set a Copy value through &T | no borrows |
RefCell<T> | borrow mutably through &T | runtime |
Rc<RefCell<T>> | several owners that can all change it | runtime |
Arc<Mutex<T>> | the same, across threads | runtime |
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.
StringandVecare 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.Derefmakes*and method calls work through a wrapper, and deref coercion turns&Box<T>,&Stringor&YourWrapperinto the reference a function expects.Dropruns cleanup when a value goes out of scope, in reverse declaration order. Drop something early withdrop(value), nevervalue.drop().Rc<T>gives a value several read-only owners, counted at runtime.Rc::clonecopies the pointer, not the data. It is single-threaded, and cycles ofRcs leak.Cell<T>andRefCell<T>allow mutation through&T.RefCellchecks 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.