Skip to main content

Smart Pointers in Rust: Rc, Arc, RefCell, and Weak Explained

Published: April 24, 2026 Updated: August 28, 2026 Larry Qu 7 min read

Rust’s ownership model gives you one owner per value by default — simple, safe, and predictable. But real data structures often need something more complex: trees where nodes reference their parents, graphs with shared edges, interpreter environments where closures capture mutable state. Smart pointers are the tools Rust gives you to handle these cases while preserving safety guarantees.

Box<T> — Heap Allocation with Single Ownership

Box<T> puts a value on the heap and gives you a single owning pointer to it. Use it when:

  • You have a recursive type that can’t be sized at compile time
  • You want to avoid copying large values
  • You need a trait object (Box<dyn Trait>)
// Recursive linked list — requires Box to break the infinite size
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

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

    // Trait object — store different types behind one interface
    trait Animal {
        fn sound(&self) -> &str;
    }

    struct Dog;
    struct Cat;

    impl Animal for Dog { fn sound(&self) -> &str { "woof" } }
    impl Animal for Cat { fn sound(&self) -> &str { "meow" } }

    let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat), Box::new(Dog)];
    for a in &animals {
        println!("{}", a.sound());
    }
}

Box<T> has no runtime overhead beyond the heap allocation itself — no reference counting, no locks.

Rc<T> — Shared Ownership (Single-Threaded)

Rc<T> (Reference Counted) lets multiple parts of your program share ownership of the same heap-allocated value. The value is dropped when the last Rc clone is dropped.

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3, 4, 5]);

    let a = Rc::clone(&data); // Cheap: increments count, no deep copy
    let b = Rc::clone(&data);

    println!("Strong count: {}", Rc::strong_count(&data)); // 3
    println!("a sum: {}", a.iter().sum::<i32>());           // 15

    drop(a);
    println!("After drop(a): {}", Rc::strong_count(&data)); // 2

    // data and b still valid
    println!("b: {:?}", b);
}

Rc is not thread-safe — it uses non-atomic operations for performance. The compiler enforces this: Rc is not Send or Sync.

Use Case: Shared Immutable Config

use std::rc::Rc;

#[derive(Debug)]
struct Config {
    max_connections: u32,
    timeout_ms: u64,
    host: String,
}

struct ConnectionPool {
    config: Rc<Config>,
}

struct RequestHandler {
    config: Rc<Config>,
}

fn main() {
    let config = Rc::new(Config {
        max_connections: 100,
        timeout_ms: 5000,
        host: "localhost".to_string(),
    });

    let pool    = ConnectionPool    { config: Rc::clone(&config) };
    let handler = RequestHandler   { config: Rc::clone(&config) };

    println!("Pool timeout: {}ms", pool.config.timeout_ms);
    println!("Handler host: {}", handler.config.host);
    println!("Config refs: {}", Rc::strong_count(&config)); // 3
}

RefCell<T> — Interior Mutability

Rust’s borrow checker enforces that you can’t have mutable and immutable references coexisting. RefCell<T> moves this check to runtime, letting you mutate data through a shared reference — a pattern called interior mutability:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    // Multiple immutable borrows at once
    let r1 = data.borrow();
    let r2 = data.borrow();
    println!("{:?} {:?}", r1, r2); // [1, 2, 3] [1, 2, 3]
    drop(r1);
    drop(r2);

    // Mutable borrow (only one at a time)
    {
        let mut w = data.borrow_mut();
        w.push(4);
    } // borrow_mut guard dropped here

    println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}

If you violate the borrow rules at runtime, RefCell panics:

let cell = RefCell::new(0);
let _r = cell.borrow();
// let _w = cell.borrow_mut(); // PANIC: already borrowed immutably

Use try_borrow() and try_borrow_mut() for non-panicking versions that return Result.

Cell<T> — For Copy Types

When T: Copy, Cell<T> is a simpler interior mutability option with no borrow guards:

use std::cell::Cell;

struct Counter {
    count: Cell<u32>,
}

impl Counter {
    fn new() -> Self { Counter { count: Cell::new(0) } }
    fn increment(&self) { self.count.set(self.count.get() + 1); }
    fn value(&self) -> u32 { self.count.get() }
}

fn main() {
    let c = Counter::new();
    c.increment();
    c.increment();
    println!("{}", c.value()); // 2
}

Rc<RefCell<T>> — Shared Mutable State (Single-Threaded)

Combining Rc and RefCell gives you multiple owners that can all mutate the shared data:

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

#[derive(Debug)]
struct Node {
    value: i32,
    neighbors: Vec<Rc<RefCell<Node>>>,
}

impl Node {
    fn new(value: i32) -> Rc<RefCell<Node>> {
        Rc::new(RefCell::new(Node { value, neighbors: Vec::new() }))
    }

    fn connect(a: &Rc<RefCell<Node>>, b: &Rc<RefCell<Node>>) {
        a.borrow_mut().neighbors.push(Rc::clone(b));
        b.borrow_mut().neighbors.push(Rc::clone(a));
    }
}

fn main() {
    let node_a = Node::new(1);
    let node_b = Node::new(2);
    let node_c = Node::new(3);

    Node::connect(&node_a, &node_b);
    Node::connect(&node_b, &node_c);

    println!("Node A value: {}", node_a.borrow().value);
    println!("Node A neighbors: {}",
        node_a.borrow().neighbors.iter()
              .map(|n| n.borrow().value.to_string())
              .collect::<Vec<_>>()
              .join(", ")
    );
}

The GUI State Pattern

A common pattern in single-threaded GUIs and interpreters:

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

type SharedState = Rc<RefCell<AppState>>;

#[derive(Debug)]
struct AppState {
    count: u32,
    messages: Vec<String>,
}

struct Button {
    state: SharedState,
    label: String,
}

struct Label {
    state: SharedState,
}

impl Button {
    fn click(&self) {
        let mut s = self.state.borrow_mut();
        s.count += 1;
        s.messages.push(format!("'{}' clicked (count={})", self.label, s.count));
    }
}

impl Label {
    fn render(&self) -> String {
        format!("Count: {}", self.state.borrow().count)
    }
}

fn main() {
    let state = Rc::new(RefCell::new(AppState {
        count: 0,
        messages: Vec::new(),
    }));

    let btn = Button { state: Rc::clone(&state), label: "Submit".to_string() };
    let lbl = Label  { state: Rc::clone(&state) };

    btn.click();
    btn.click();

    println!("{}", lbl.render()); // Count: 2
    for msg in &state.borrow().messages {
        println!("{}", msg);
    }
}

Arc<T> — Shared Ownership Across Threads

Arc<T> (Atomically Reference Counted) is the thread-safe version of Rc<T>. It uses atomic operations for the reference count, making it Send and Sync. The cost: slightly slower reference counting than Rc.

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);
    let mut handles = Vec::new();

    for tid in 0..4 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let sum: i32 = data.iter().sum();
            println!("Thread {}: sum = {}", tid, sum);
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    println!("Main: data still valid: {:?}", data);
}

Pair with Mutex or RwLock for mutable shared state across threads — see Concurrency in Rust for patterns.

Weak<T> — Non-Owning References

Weak<T> is a non-owning reference to an Rc or Arc-managed value. It does not prevent the value from being dropped. Use it to break reference cycles and express “I reference this, but I don’t own it”:

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

#[derive(Debug)]
struct TreeNode {
    value: i32,
    parent: RefCell<Weak<TreeNode>>,   // Non-owning upward reference
    children: RefCell<Vec<Rc<TreeNode>>>, // Owning downward references
}

impl TreeNode {
    fn new(value: i32) -> Rc<Self> {
        Rc::new(TreeNode {
            value,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }

    fn add_child(parent: &Rc<TreeNode>, child: Rc<TreeNode>) {
        *child.parent.borrow_mut() = Rc::downgrade(parent);
        parent.children.borrow_mut().push(child);
    }
}

fn main() {
    let root  = TreeNode::new(1);
    let child = TreeNode::new(2);
    let leaf  = TreeNode::new(3);

    TreeNode::add_child(&root, Rc::clone(&child));
    TreeNode::add_child(&child, Rc::clone(&leaf));

    // Navigate up: upgrade() returns Option<Rc<T>>
    let parent = leaf.parent.borrow().upgrade();
    match parent {
        Some(p) => println!("leaf's parent: {}", p.value), // 2
        None    => println!("no parent"),
    }

    println!("root strong count: {}", Rc::strong_count(&root));   // 1
    println!("child strong count: {}", Rc::strong_count(&child)); // 2 (root + child var)
    println!("child weak count: {}", Rc::weak_count(&child));     // 1 (leaf's parent ref)
}

Observer Pattern with Weak References

Weak is perfect for observer/event patterns where subjects shouldn’t keep observers alive:

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

trait Observer {
    fn on_event(&self, event: &str);
}

struct EventBus {
    subscribers: Vec<Weak<dyn Observer>>,
}

impl EventBus {
    fn new() -> Self { EventBus { subscribers: Vec::new() } }

    fn subscribe(&mut self, obs: &Rc<dyn Observer>) {
        self.subscribers.push(Rc::downgrade(obs));
    }

    fn publish(&mut self, event: &str) {
        // Remove dead subscribers, notify live ones
        self.subscribers.retain(|weak| {
            if let Some(obs) = weak.upgrade() {
                obs.on_event(event);
                true
            } else {
                false // subscriber was dropped, remove it
            }
        });
    }
}

When to Use Each Smart Pointer

Situation Use
Single owner, heap allocation Box<T>
Trait objects Box<dyn Trait>
Shared ownership, single thread Rc<T>
Shared ownership, multi-thread Arc<T>
Interior mutability (Copy types) Cell<T>
Interior mutability (non-Copy) RefCell<T>
Shared mutable state, single thread Rc<RefCell<T>>
Shared mutable state, multi-thread Arc<Mutex<T>>
Back-reference (break cycles) Weak<T>

Performance Notes

  • Box<T>: zero overhead beyond heap allocation
  • Rc<T>: non-atomic refcount — fast, single-thread only
  • Arc<T>: atomic refcount — slightly slower than Rc, thread-safe
  • RefCell<T>: runtime borrow check — small overhead per borrow
  • Cell<T>: cheaper than RefCell for Copy types — no guard needed

Prefer Box<T> and plain ownership where possible. Reach for Rc/Arc only when you genuinely need shared ownership.

Summary

Smart pointers express ownership relationships that go beyond Rust’s default single-owner model:

  • Box<T> — owned heap value, single owner
  • Rc<T> — shared ownership, single thread
  • Arc<T> — shared ownership, any thread
  • RefCell<T> — interior mutability with runtime borrow checks
  • Weak<T> — non-owning reference, breaks cycles

The combination Rc<RefCell<T>> (or Arc<Mutex<T>> for threads) covers the vast majority of advanced ownership patterns in real Rust code.

Resources

Comments

👍 Was this article helpful?