Until now, one variable owned each value and references borrowed it temporarily. That is still the best starting point. Some designs need an owned value behind a pointer, several owners of one value, or mutation through a shared reference. Rust makes those choices explicit with library types.
A smart pointer holds access to a value and manages some part of its lifetime or ownership. Box owns a heap allocation; Rc counts shared owners. Interior mutability means a type offers controlled mutation through a shared reference. It is a separate capability: Rc alone does not grant mutable access.
This lesson follows closures and iterators. You will build a recursive value, share a small activity log, observe cleanup, and break an ownership cycle.
Run the examples
Create a scratch project:
cargo new pointers-lab --edition 2024
cd pointers-lab
Each Rust block containing fn main is a separate, complete src/main.rs. Replace that file with one block, then run cargo run. These examples use only the standard library.
Choose ownership and mutation separately
| Type | What it provides | What it does not provide |
|---|---|---|
Box<T> | One owner of a heap-allocated value | Shared ownership |
Rc<T> | Reference-counted ownership within one thread | General mutable access to T, or transfer between threads |
Weak<T> from std::rc | A non-owning connection to an Rc allocation | A guarantee that the value still exists |
Cell<T> | Replacement of a value through &self; copying it out when T: Copy | Borrowing the stored value through &Cell<T> |
RefCell<T> | Runtime-checked shared or exclusive borrows through &self | Thread-safe shared mutation |
Arc<T> | Atomic reference-counted ownership, subject to T's thread-safety bounds | Automatic thread safety for arbitrary T |
Prefer &T when an owner already outlives the work, &mut T when you can arrange exclusive access, and owned fields when sharing is unnecessary. More wrappers mean more ownership rules to explain.
Box: indirection gives recursive types a finite size
A value must have a known size when stored directly in another value. An enum whose next node contains another copy of the same enum without indirection would require an infinite amount of space. A Box has a fixed-size pointer representation for a sized pointee, so the recursive part lives behind that pointer.
#[derive(Debug)]
enum Chain {
End,
Link(i32, Box<Chain>),
}
impl Chain {
fn total(&self) -> i32 {
match self {
Self::End => 0,
Self::Link(value, rest) => value + rest.total(),
}
}
}
fn character_count(text: &str) -> usize {
text.chars().count()
}
fn main() {
let chain = Chain::Link(3, Box::new(Chain::Link(5, Box::new(Chain::End))));
assert_eq!(chain.total(), 8);
let name = Box::new(String::from("Rust"));
assert_eq!(character_count(&name), 4);
println!("total: {}", chain.total());
}
Output:
total: 8
Moving a box transfers ownership of its allocation. Dropping it drops the contained value and releases the allocation. Boxing an already heap-backed String is usually unnecessary; it is here to demonstrate dereferencing.
The character_count call uses deref coercion: Rust follows the Deref implementations from &Box<String> to &String and then to &str. The * operator can explicitly dereference a pointer, while coercion adapts references at supported sites such as function arguments. Implementing Deref for your own wrapper should mean that the wrapper behaves like its target; it is not a substitute for arbitrary type conversion.
This linked structure is a teaching example. A Vec usually gives better locality for a sequence. Very deep recursive traversal or recursive destruction can overflow the stack, even though the nodes live on the heap. See the Book on Box and Deref.
Drop: cleanup follows ownership
Drop::drop runs when a value is destroyed. This underlies releasing allocations, closing owned file handles and unlocking mutex guards. The pattern is often called RAII: acquiring a resource produces a value whose destruction releases it.
struct Session(&'static str);
impl Drop for Session {
fn drop(&mut self) {
println!("close {}", self.0);
}
}
fn main() {
let first = Session("first");
let second = Session("second");
println!("working");
drop(first);
println!("first released");
let _keep_until_end = second;
}
Output:
working
close first
first released
close second
Call the ordinary drop(value) function to move a value into early destruction. Calling value.drop() directly is forbidden. Local variables normally drop in reverse declaration order; struct fields drop in declaration order after the struct's own Drop code.
Do not treat destructors as an error-reporting channel. Drop::drop cannot return an error, and process abort or std::process::exit skips normal stack cleanup. Explicitly flush or finish a resource when an error matters. The Book explains Drop and early cleanup.
Rc: cloning a handle shares the same value
Rc::clone increments a strong reference count. It does not copy the underlying allocation. The last strong owner dropping destroys the value.
use std::rc::Rc;
fn main() {
let title = Rc::new(String::from("Learning Rust"));
let sidebar = Rc::clone(&title);
assert!(Rc::ptr_eq(&title, &sidebar));
assert_eq!(Rc::strong_count(&title), 2);
{
let preview = Rc::clone(&title);
assert_eq!(preview.as_str(), "Learning Rust");
assert_eq!(Rc::strong_count(&title), 3);
}
assert_eq!(Rc::strong_count(&title), 2);
drop(sidebar);
assert_eq!(Rc::strong_count(&title), 1);
println!("{}", title);
}
Output: Learning Rust.
The explicit Rc::clone(&title) spelling makes sharing visible. A plain String::clone instead creates independently owned string data. Counts are useful for learning, but application correctness should not depend on a count staying fixed across unrelated operations. The Rc documentation describes its single-threaded ownership model.
RefCell: enforce borrow rules at runtime
Imagine two parts of a single-threaded application writing to the same log. Rc<RefCell<Vec<String>>> combines shared ownership with checked mutation:
use std::cell::RefCell;
use std::rc::Rc;
fn record(log: &RefCell<Vec<String>>, event: &str) {
log.borrow_mut().push(event.to_owned());
}
fn main() {
let log = Rc::new(RefCell::new(Vec::<String>::new()));
let editor = Rc::clone(&log);
record(&log, "opened");
record(&editor, "saved");
{
let snapshot = log.borrow();
assert_eq!(*snapshot, ["opened", "saved"]);
assert!(editor.try_borrow_mut().is_err());
}
editor.borrow_mut().push(String::from("closed"));
assert_eq!(log.borrow().len(), 3);
println!("events: {}", log.borrow().len());
}
Output: events: 3.
borrow() returns a Ref guard. borrow_mut() returns a RefMut guard. These guards keep the runtime borrow active until they are dropped. While any shared guard exists, a mutable borrow is rejected; while a mutable guard exists, any other borrow is rejected.
borrow and borrow_mut panic on a conflict. Their try_ counterparts return a Result, which lets you handle a conflict without panicking. The example intentionally uses try_borrow_mut to observe a conflict safely. No borrow rule has disappeared; the check has moved from compilation to execution.
Use small scopes for guards. Calling another function while holding a guard can accidentally re-enter the same cell and cause a conflict. A RefCell can also be useful without Rc, such as a cache inside a single owner. Read the Book's interior mutability explanation and the RefCell API.
Cell: copy or replace a small value
For a counter, runtime borrowing is often unnecessary. Cell<u32> can copy the integer out and replace it through a shared reference:
use std::cell::Cell;
#[derive(Default)]
struct Meter {
reads: Cell<u32>,
}
impl Meter {
fn read(&self) -> &'static str {
self.reads.set(self.reads.get() + 1);
"ready"
}
}
fn main() {
let meter = Meter::default();
assert_eq!(meter.read(), "ready");
assert_eq!(meter.read(), "ready");
assert_eq!(meter.reads.get(), 2);
println!("reads: {}", meter.reads.get());
}
Output: reads: 2. get requires Copy. Other methods such as replace can move a non-Copy value out while installing a replacement. Cell is not a lock and cannot be shared by reference across threads. See Cell.
Weak: a connection that does not keep a value alive
If a parent strongly owns a child and the child strongly owns the parent, neither strong count reaches zero. Rust permits this memory leak. Make the back-reference weak instead.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Folder {
name: String,
parent: RefCell<Weak<Folder>>,
children: RefCell<Vec<Rc<Folder>>>,
}
fn folder(name: &str) -> Rc<Folder> {
Rc::new(Folder {
name: name.to_owned(),
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn main() {
let child = folder("notes");
{
let root = folder("home");
*child.parent.borrow_mut() = Rc::downgrade(&root);
root.children.borrow_mut().push(Rc::clone(&child));
let parent = child.parent.borrow().upgrade().expect("root is alive");
assert_eq!(parent.name, "home");
}
assert!(child.parent.borrow().upgrade().is_none());
println!("{} has no live parent", child.name);
}
Output: notes has no live parent.
upgrade() returns Option<Rc<T>>: it temporarily creates a strong owner only if the value still exists. Weak handles can keep the allocation's bookkeeping around after the value has been dropped, but they do not keep the value itself alive. See reference cycles.
Idioms and pitfalls
- Do not wrap every field in
Rc<RefCell<_>>. First try passing&mutor giving one component clear ownership. - A shared
Rcclone is not a snapshot. Every handle observes the same contained value. - Do not return a reference borrowed from a temporary
Refguard. Return an owned result, or deliberately return a guard with the appropriate lifetime. RefCellguards last until they drop, even if you have stopped reading through them. End the scope or calldrop(guard)before taking an incompatible borrow.RcandWeakfromstd::rccannot cross thread boundaries.Arc<RefCell<T>>does not solve that problem; the inner type still lacks thread-safe shared mutation.- Shared ownership can leak through cycles. Prefer weak back-references or an arena of nodes addressed by indices when that makes the model clearer.
Exercises
1. Release a runtime borrow
Create a RefCell<Vec<i32>> containing 10. Borrow it for reading, prove that try_borrow_mut fails, release the read guard, append 20, and assert that the sum is 30.
2. Choose a representation
Choose a type for each case and explain the ownership: one large owned value behind a pointer; several views sharing immutable text on one thread; a parent link that must not keep its parent alive; a small per-object read count updated through &self.
3. Diagnose the log bug
A callback holds let guard = log.borrow_mut() and calls record(&log, "nested") before dropping guard. Why does it panic? Give two design changes that avoid the conflict without unsafe.
Solutions
1. Scope the guard
use std::cell::RefCell;
fn main() {
let numbers = RefCell::new(vec![10]);
{
let read = numbers.borrow();
assert_eq!(read[0], 10);
assert!(numbers.try_borrow_mut().is_err());
}
numbers.borrow_mut().push(20);
assert_eq!(numbers.borrow().iter().sum::<i32>(), 30);
}
2. Match the capability
Use Box<T>, Rc<String> (or borrowed &str views if ownership permits), Weak<Parent>, and Cell<u32>, respectively. None of these choices implies that a second thread may mutate the data.
3. Avoid re-entering a borrowed cell
The nested call requests a second mutable borrow while the first RefMut is active. End the guard's scope before the callback, or change the helper to accept &mut Vec<String> and pass it the existing guard's mutable access. Both make the exclusive access explicit.
Key takeaways
Boxsupplies owned indirection,Rcsupplies shared ownership, andWeaksupplies a fallible non-owning connection.Derefadapts access to a target;Dropreleases resources when an owner is destroyed.Cellreplaces values andRefCellchecks borrows at runtime. Neither supplies synchronized shared mutation across threads.- Keep borrow guards short and make ownership cycles visible in your design.
Next lesson
Continue with Threads and channels in Rust, where ownership moves between operating-system threads.

