[Rust Series 16] Smart Pointers and Heap Data

한국어 버전

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 owners
  • RefCell<T>: interior mutation
  • Rc<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.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Practice

  1. Extend the Node enum with a value() method that returns the last Value variant.
  2. Design a shared event log using Rc<RefCell<Vec<String>>> and let each module push entries.

Wrap-Up

Smart pointers are syntactic tools for safely working with heap data. Box<T> represents unsized or recursive types, Rc<T> enables shared immutable ownership, and RefCell<T> gives runtime mutability. Next time we'll focus on testing and documentation flows that keep these structures reliable.

💬 댓글

이 글에 대한 의견을 남겨주세요