Skip to content
AgileLogics
7 min read

Smart Pointers: Box, Rc & RefCell

Prerequisites: Pattern matching in depth

A smart pointer is a struct that acts like a reference but also owns or manages the data it points to. String and Vec<T> are smart pointers already. This lesson covers the three you will meet most often:

Type What it gives you Checked
Box<T> a single owner, data stored on the heap compile time
Rc<T> several owners of the same read-only data (reference counted) compile time
RefCell<T> mutation through a shared reference ("interior mutability") runtime

All three implement Deref, so you can call methods of the inner value directly (boxed.len()), and Drop, so the data is freed automatically.

Box: heap allocation and recursive types

Box::new(value) moves the value to the heap and keeps a pointer on the stack. The main uses are:

  • Recursive types, whose size would otherwise be infinite. A list node that contains another list node must hold it behind a pointer.
  • Trait objects such as Box<dyn Error> or Box<dyn Fn()>.
  • Moving a large value around without copying its bytes.

Rc: shared ownership

Rc<T> ("reference counted") lets several parts of a program own the same value. Rc::clone(&rc) doesn't copy the data; it increments a counter. The value is dropped when the last Rc goes away. Rc gives only shared (&) access, and it is for single-threaded code only — the thread-safe version, Arc, comes in a later lesson.

RefCell: interior mutability

RefCell<T> moves the borrowing rules from compile time to runtime. .borrow() returns a shared borrow and .borrow_mut() a mutable one; if you break the rules (for example two borrow_mut calls alive at once), the program panics instead of failing to compile. Combined as Rc<RefCell<T>>, it gives several owners that can all modify the same value.

Syntax

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

fn main() {
    let b = Box::new(5);                          // an i32 on the heap
    let shared = Rc::new(String::from("hi"));
    let other = Rc::clone(&shared);                // two owners, one String
    let cell = RefCell::new(vec![1]);
    cell.borrow_mut().push(2);                     // mutate through a shared reference
    println!("{} {} {} {:?}", b, other, Rc::strong_count(&shared), cell.borrow());
}

Example

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

// A recursive type needs Box: List contains another List.
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn sum(list: &List) -> i32 {
    match list {
        Cons(value, rest) => value + sum(rest),
        Nil => 0,
    }
}

#[derive(Debug)]
struct Account {
    owner: String,
    balance: i64,
}

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("{list:?}");
    println!("sum = {}", sum(&list));

    // Rc<RefCell<T>>: two owners that can both modify the same account.
    let account = Rc::new(RefCell::new(Account { owner: String::from("Ada"), balance: 100 }));
    let alice_view = Rc::clone(&account);
    let bank_view = Rc::clone(&account);
    println!("owners: {}", Rc::strong_count(&account));

    alice_view.borrow_mut().balance -= 30;
    bank_view.borrow_mut().balance += 5;
    println!("{} has {}", account.borrow().owner, account.borrow().balance);

    drop(alice_view);
    println!("owners after drop: {}", Rc::strong_count(&account));

    // try_borrow_mut reports a conflict instead of panicking.
    let reading = account.borrow();
    match account.try_borrow_mut() {
        Ok(_) => println!("got a mutable borrow"),
        Err(_) => println!("already borrowed: cannot mutate while reading"),
    }
    drop(reading);
    println!("free again: {}", account.try_borrow_mut().is_ok());
}

Expected output

Cons(1, Cons(2, Cons(3, Nil)))
sum = 6
owners: 3
Ada has 75
owners after drop: 2
already borrowed: cannot mutate while reading
free again: true

Weak: breaking reference cycles

If two values hold Rcs to each other, their counts never reach zero and the memory leaks. Weak<T>, created with Rc::downgrade(&rc), is a pointer that doesn't keep the value alive. Call .upgrade() to get an Option<Rc<T>>: Some if the value still exists, None if it has been dropped. A typical use is a tree where parents own children with Rc and children point back to their parent with Weak.

use std::rc::{Rc, Weak};

fn main() {
    let strong = Rc::new(String::from("parent"));
    let weak: Weak<String> = Rc::downgrade(&strong);
    println!("{:?}", weak.upgrade());   // Some("parent")
    drop(strong);
    println!("{:?}", weak.upgrade());   // None
}

Common mistakes

  • Defining a recursive type without indirection: enum List { Cons(i32, List), Nil } fails with error[E0072]: recursive type `List` has infinite size. Wrap the inner value in Box.
  • Holding a borrow() while calling borrow_mut() on the same RefCell. It compiles, then panics at runtime (Rust 1.85 prints already borrowed: BorrowMutError; newer versions say RefCell already borrowed). Keep borrows short, or use try_borrow_mut.
  • Trying to mutate through a plain Rc<T>: error[E0594]: cannot assign to data in an `Rc` . Put a RefCell inside.
  • Writing shared.clone() and wondering whether it deep-copies. On an Rc it only bumps the count, but Rc::clone(&shared) makes that obvious to readers.
  • Reaching for Rc<RefCell<T>> by default. Plain ownership and borrowing are simpler and checked at compile time; use these types when shared ownership is genuinely needed.

Practice

Create an Rc<RefCell<Vec<String>>> log shared by two "components" (two Rc clones). Push one message through each clone, then print the number of messages, the messages joined by " | ", and the strong count.

Show solution
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let log = Rc::new(RefCell::new(Vec::<String>::new()));
    let network = Rc::clone(&log);
    let storage = Rc::clone(&log);

    network.borrow_mut().push(String::from("connected"));
    storage.borrow_mut().push(String::from("saved file"));

    let entries = log.borrow();
    println!("{} messages", entries.len());
    println!("{}", entries.join(" | "));
    println!("strong count: {}", Rc::strong_count(&log));
}
2 messages
connected | saved file
strong count: 3