What You'll Learn
This entry starts by addressing the confusion that usually accompanies the term smart pointer. A smart pointer is both a value and a pointer-like type, and Box<T>, [[rc|Rc<T>]], and [[refcell|RefCell<T>]] are the most common ones. We'll clarify when you need each type and which ownership limitations they relax.
Why We Need Them
Rust closely tracks ownership for stack-resident values. Recursive data structures or scenarios where several owners must access the same data are hard to express with stack values alone. In those cases we need heap allocation plus pointer management, and smart pointers let us manage heap data and ownership rules together.
For beginners, the easiest starting question is: why can't plain references solve this?
- &T: temporarily borrow a value
- [[box-t|Box]]: keep one owner, but move the value onto the heap
- [[rc|Rc]]: allow multiple owners to share read-only access
- [[refcell|RefCell]]: allow mutation with borrow checks enforced at runtime
So smart pointers are not just "pointer syntax." They are tools for reshaping ownership rules.
Lifting Values with Box
Box<T> places the value on the heap while keeping only a pointer on the stack. It's helpful for building recursive types or moving large data without actually copying it.
enum Node {
Value(i32),
Next(Box<Node>),
}
fn depth(node: &Node) -> u32 {
match node {
Node::Value(_) => 1,
Node::Next(inner) => 1 + depth(inner),
}
}
fn main() {
let chain = Node::Next(Box::new(Node::Next(Box::new(Node::Value(10)))));
println!("depth = {}", depth(&chain));
}
Enums that refer to themselves must still have a known size. Because Box itself has pointer size, it removes that limitation.
Sharing with [[rc|Rc<T>]]
[[rc|Rc<T>]] (Reference Counted) lets multiple owners share the same heap data. The compiler still enforces exclusive mutable access, so Rc<T> only offers shared immutable access. Rc<T> is also single-threaded; to share across threads you'll need [[arc|Arc<T>]], which we'll cover in part 18.
use std::rc::Rc;
struct TodoItem {
title: String,
}
fn main() {
let shared = Rc::new(TodoItem { title: "Review ownership".into() });
let daily = Rc::clone(&shared);
let review = Rc::clone(&shared);
println!("daily = {}", daily.title);
println!("review = {}", review.title);
println!("count = {}", Rc::strong_count(&shared));
}
Rc::clone increments the reference count without copying the data. In other words, this is not like cloning a String; it is more like adding one more shared handle to the same value. Because thread sharing is off-limits, keep in mind that you'll need Arc<T> once concurrency enters the picture.
Getting Runtime Mutability with [[refcell|RefCell<T>]]
[[refcell|RefCell<T>]] lets you mutate inner data even when you hold only an immutable reference, but breaking the borrowing rules now triggers a runtime panic. In other words, it moves the borrow check from compile time to runtime as an interior mutability tool.
In short:
- Compile-time checks: stricter but safer
- Runtime checks (
RefCell): more flexible but can panic while running
use std::cell::RefCell;
fn main() {
let log = RefCell::new(Vec::new());
{
let mut borrow = log.borrow_mut();
borrow.push("start".to_string());
}
println!("entries = {:?}", log.borrow());
}
The borrow and borrow_mut methods return references while tracking how many borrows are alive. Multiple immutable borrows are fine, but overlapping mutable borrows will panic at runtime rather than compile time.
Combining Rc<RefCell>
Whenever several owners must share data and a subset of them needs to mutate it, reach for the Rc<RefCell<T>> pattern.
This is the hardest combination in the chapter, so it helps to split the roles apart:
Rc<T>: multiple ownersRefCell<T>: interior mutationRc<RefCell<T>>: multiple owners sharing one mutable value in a single thread
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Counter {
value: u32,
}
fn main() {
let counter = Rc::new(RefCell::new(Counter { value: 0 }));
let job_a = Rc::clone(&counter);
let job_b = Rc::clone(&counter);
{
job_a.borrow_mut().value += 1;
}
{
job_b.borrow_mut().value += 2;
}
println!("final value = {}", counter.borrow().value);
}
You'll see this pattern in GUI component trees and graph structures. Always double-check the mutation paths to avoid runtime panics, and beware of Rcs that reference each other, since cycles prevent memory from being freed. For complex graphs, pair this approach with tools such as [[weak|Weak<T>]].
A good beginner summary is this: Rc<RefCell<T>> is powerful, but the compiler no longer catches everything for you, so you have to be more deliberate.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.
💬 댓글
이 글에 대한 의견을 남겨주세요