Rust Smart Pointers
A pointer is a value that tells you where some other data lives in memory. References like &x are the simplest kind. Smart pointers are pointers with extra abilities: they own the data they point to, and they clean it up when they are done. You have already used two of them without the name: String and Vec are both smart pointers to text and items stored on the heap, the part of memory for data whose size is decided while the program runs.
This topic introduces three smart pointers you will meet in real Rust code. You do not need them often as a beginner, but it is good to know what they are for.
Box: a value on the heap
Box<T> puts a single value on the heap and keeps a pointer to it. Using a boxed value feels just like using the value itself:
fn main() {
let boxed = Box::new(41);
let answer = *boxed + 1;
println!("{} and {}", boxed, answer);
}41 and 42
The main reason to use a Box is a type that contains itself. A list where each item points to the next one cannot have a fixed size, because it could be any length. Putting the next item in a Box gives it a known size, a single pointer:
enum List {
Item(i32, Box<List>),
End,
}
fn total(list: &List) -> i32 {
match list {
List::Item(value, rest) => value + total(rest),
List::End => 0,
}
}
fn main() {
let list = List::Item(3, Box::new(List::Item(4, Box::new(List::Item(5, Box::new(List::End))))));
println!("The list adds up to {}", total(&list));
}The list adds up to 12
Rc: more than one owner
Normally every value has exactly one owner. Sometimes several parts of a program genuinely need to share one value, like several people sharing one family car. Rc<T>, for reference counted, keeps count of how many owners there are, and frees the value only when the last one is gone:
use std::rc::Rc;
fn main() {
let car = Rc::new(String::from("blue hatchback"));
let for_mum = Rc::clone(&car);
let for_dad = Rc::clone(&car);
println!("Everyone shares the {}", for_mum);
println!("Owners: {}", Rc::strong_count(&car));
drop(for_dad);
println!("Owners after one gives it up: {}", Rc::strong_count(&car));
}Everyone shares the blue hatchback Owners: 3 Owners after one gives it up: 2
Rc::clone does not copy the string; it only adds one to the count, which is very cheap.
RefCell: changing shared data
An Rc only gives read access, because many owners changing the same data at once would break the borrowing rules. RefCell<T> moves the borrow check from compile time to run time: you ask with borrow_mut() while the program runs, and it panics if the rules would be broken. Together, Rc<RefCell<T>> gives you shared data that can change:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let scoreboard = Rc::new(RefCell::new(Vec::new()));
let team_a = Rc::clone(&scoreboard);
let team_b = Rc::clone(&scoreboard);
team_a.borrow_mut().push("A scores");
team_b.borrow_mut().push("B scores");
team_a.borrow_mut().push("A scores again");
println!("{:?}", scoreboard.borrow());
}["A scores", "B scores", "A scores again"]
Which one when?
Use plain values and references first; they are the simplest and fastest. Reach for Box for recursive types, Rc when ownership truly has to be shared, and RefCell inside an Rc when that shared data must change. If you find yourself using them everywhere, it is often a sign that the program's structure could be simpler.
Practice exercises
- Box a
Stringand print its length through the box. - Add a function
count(list: &List) -> u32that counts the items of the boxed list. - Share one
Rc<String>between three variables and print the count after eachdrop.