EthicsComputerCourses
Join training +91 90532 10052

Smart Pointers: Box, Rc & RefCell

Rust · Lesson 27 of 43 Advanced 7 min read Updated

Ownership says every value has exactly one owner, and borrowing says you may change a value only through one &mut at a time. These rules cover most programs. Some designs need more: a type that contains itself (a tree), a value with several owners (a playlist shared by two users), or data that must change while it is shared.

For these cases Rust has smart pointers: structs that act like references but also own their data and add an extra ability. You have already used two, String and Vec, which own heap memory. In this lesson you will learn Box, Rc, RefCell and Weak, and when each one is the right tool.

Box: a value on the heap

Box<T> stores a value on the heap and keeps only a pointer on the stack. It has a single owner, and when the box is dropped, so is the value. You use a box just like the value inside, thanks to the Deref trait:

Rust
enum Expr {
    Num(i64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

fn eval(e: &Expr) -> i64 {
    match e {
        Expr::Num(n) => *n,
        Expr::Add(a, b) => eval(a) + eval(b),
        Expr::Mul(a, b) => eval(a) * eval(b),
    }
}

fn main() {
    let price = Box::new(1299);
    println!("Price on the heap: {price}");
    println!("With GST: {}", *price * 118 / 100);

    // (2 + 3) * 4
    let expr = Expr::Mul(
        Box::new(Expr::Add(Box::new(Expr::Num(2)), Box::new(Expr::Num(3)))),
        Box::new(Expr::Num(4)),
    );
    println!("(2 + 3) * 4 = {}", eval(&expr));
}
Output
Price on the heap: 1299
With GST: 1532
(2 + 3) * 4 = 20

Boxing a single number, as with price, is only a demonstration. The real use is the Expr enum. An Expr can contain other Expr values, so without the boxes its size would be infinite: an Add holds an Expr, which might hold an Add, and so on. The compiler rejects that with error E0072, "recursive type Expr has infinite size", and suggests adding indirection such as a Box. A box always has the size of one pointer, so the enum gets a fixed size.

Box has three common uses:

  • Recursive types such as trees, expression trees and linked lists.
  • Trait objects, such as Box<dyn Fare> from the Traits lesson.
  • Large values that you want to move around cheaply: moving a box copies only the pointer.

Rc: shared ownership

Sometimes a value really has several owners, and none of them knows which will finish last. Rc<T> (reference counted) keeps a count of owners. Rc::clone does not copy the data; it adds one to the count and returns another pointer to the same value. When the last Rc is dropped, the count reaches zero and the value is freed.

Rust
use std::rc::Rc;

struct Playlist {
    name: String,
    songs: Vec<String>,
}

fn main() {
    let road_trip = Rc::new(Playlist {
        name: String::from("Road trip"),
        songs: vec![String::from("Track 1"), String::from("Track 2")],
    });
    println!("Owners: {}", Rc::strong_count(&road_trip));

    let priya_library = Rc::clone(&road_trip);
    let rohan_library = Rc::clone(&road_trip);
    println!("Owners: {}", Rc::strong_count(&road_trip));
    println!("Priya sees {} songs", priya_library.songs.len());

    drop(rohan_library);
    println!("Owners: {}", Rc::strong_count(&road_trip));
    println!("Still here: {}", road_trip.name);
}
Output
Owners: 1
Owners: 3
Priya sees 2 songs
Owners: 2
Still here: Road trip

Two limits matter. An Rc gives only shared access, so you cannot change the playlist through it. And Rc is for one thread only: its counter is not safe to update from several threads at once, and the compiler stops you from sending it to another thread. The thread-safe version, Arc, is covered in Fearless Concurrency.

RefCell: borrow checking at run time

The normal borrowing rules are checked at compile time. Occasionally you know a program is correct but the compiler cannot prove it, typically when shared data needs to change. RefCell<T> moves the borrow check to run time: borrow() gives a shared borrow, borrow_mut() gives a mutable borrow, and RefCell counts them while the program runs. This is called interior mutability, because you change the inside of a value that is not declared mut.

Rust
use std::cell::RefCell;

fn main() {
    let cart = RefCell::new(vec![String::from("rice")]);

    cart.borrow_mut().push(String::from("dal"));
    println!("Items: {:?}", cart.borrow());

    let reading = cart.borrow();
    println!("Can change now? {}", cart.try_borrow_mut().is_ok());
    drop(reading);
    println!("Can change now? {}", cart.try_borrow_mut().is_ok());
}
Output
Items: ["rice", "dal"]
Can change now? false
Can change now? true

The rules are the same as always (many readers or one writer), but a violation is no longer a compile error. If you call borrow_mut() while a borrow() is still alive, the program panics. Here we used try_borrow_mut, which returns an error instead of panicking, to show the check. Keep RefCell borrows short, ideally within one statement, as on the push line. For simple Copy values such as a counter, Cell<T> with get and set is lighter still.

Rc and RefCell together

Rc<RefCell<T>> is the classic combination for single-threaded data that has several owners and must change: Rc provides the sharing, RefCell provides the mutability.

Rust
use std::cell::RefCell;
use std::rc::Rc;

struct Account {
    owner: String,
    balance: u32,
}

fn main() {
    let joint = Rc::new(RefCell::new(Account {
        owner: String::from("Sharma family"),
        balance: 10_000,
    }));

    let parent = Rc::clone(&joint);
    let daughter = Rc::clone(&joint);

    parent.borrow_mut().balance += 5_000;
    daughter.borrow_mut().balance -= 2_500;

    let account = joint.borrow();
    println!("{}: Rs. {}", account.owner, account.balance);
    println!("Card holders: {}", Rc::strong_count(&joint));
}
Output
Sharma family: Rs. 12500
Card holders: 3

Each borrow_mut() lasts only until the end of its statement, so the two updates never overlap and nothing panics.

Weak: breaking reference cycles

If two Rc values point at each other (a parent that owns its children, and children that own their parent), neither count can ever reach zero, and the memory leaks. Rust's safety rules do not prevent leaks. The fix is Weak<T>: a non-owning pointer created with Rc::downgrade(&rc). A weak pointer does not keep the value alive; call upgrade() to get an Option<Rc<T>>, which is None if the value is already gone. In a tree, parents hold children with Rc and children point back to parents with Weak.

TypeOwnersCan change data?CheckedThreads
Box<T>OneYes, if the box is mutCompile timeCan move between threads
Rc<T>ManyNoCompile timeOne thread only
RefCell<T>OneYes, through borrow_mut()Run time (panics)Can move, but not be shared, between threads
Rc<RefCell<T>>ManyYesRun timeOne thread only
Weak<T>None (observes an Rc)Noupgrade() may give NoneOne thread only

Common mistakes

  • Reaching for Rc<RefCell<T>> first. It moves errors to run time. Try plain ownership and borrowing first, then Box, and use shared mutable state only when the design truly needs it.
  • Holding a borrow() while calling borrow_mut(). This panics at run time. Keep borrows short or use try_borrow_mut.
  • Creating Rc cycles. They leak memory. Use Weak for back-references.
  • Trying to share Rc or RefCell between threads. The compiler refuses; use Arc and Mutex instead.

Key points

  • Smart pointers own their data and add abilities; String and Vec are smart pointers too.
  • Box<T> puts a value on the heap with one owner; it enables recursive types and trait objects.
  • Rc<T> allows several owners in one thread by counting references; Rc::clone only increases the count.
  • RefCell<T> checks borrowing rules at run time and allows changing shared data; violations panic.
  • Rc<RefCell<T>> gives shared, mutable data in one thread, and Weak<T> breaks reference cycles.

Exercise

Model a folder tree with Box. Create an enum Item with variants File { name: String, size_kb: u32 } and Folder { name: String, children: Vec<Item> } (a Vec already stores items on the heap, so no Box is needed here). Write a recursive function total_size(item: &Item) -> u32, and build a small tree for a "Projects" folder containing two files and a "Photos" sub-folder with one file. Print the total size.

Show solution
Rust
enum Item {
    File { name: String, size_kb: u32 },
    Folder { name: String, children: Vec<Item> },
}

fn total_size(item: &Item) -> u32 {
    match item {
        Item::File { size_kb, .. } => *size_kb,
        Item::Folder { children, .. } => {
            let mut sum = 0;
            for child in children {
                sum += total_size(child);
            }
            sum
        }
    }
}

fn main() {
    let projects = Item::Folder {
        name: String::from("Projects"),
        children: vec![
            Item::File { name: String::from("report.pdf"), size_kb: 820 },
            Item::File { name: String::from("budget.xlsx"), size_kb: 64 },
            Item::Folder {
                name: String::from("Photos"),
                children: vec![Item::File { name: String::from("team.jpg"), size_kb: 2_300 }],
            },
        ],
    };
    println!("Total size: {} KB", total_size(&projects));
}
Output
Total size: 3184 KB

Vec<Item> breaks the infinite size just like Box, because the vector stores its items on the heap and keeps only a pointer, length and capacity inline. The compiler will warn that the name fields are never read; a real program would print them.

Quick Quiz

1. Why does a recursive enum like enum Expr { Num(i64), Add(Expr, Expr) } fail to compile?

2. What does Rc::clone(&shared) do?

3. What happens if you call borrow_mut() on a RefCell while a borrow() is still active?

4. Which pointer should a child node use to refer back to its parent to avoid a memory leak?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call