Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Rust
  3. Rust Smart Pointers Practice: Box, Rc, RefCell & Arc
Rust39 exercises

Rust Smart Pointers Practice: Box, Rc, RefCell & Arc

Practice Rust smart pointers including Box for heap allocation, Rc/Arc for shared ownership, and RefCell for interior mutability.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Create a Box containing the value 5 and store it in a variable `b`.

On this page
  1. 1You Might Not Need a Smart Pointer
  2. 2Compiler Error E0072: Recursive Type Has Infinite Size
  3. 3Compiler Error E0277: Rc Cannot Be Sent Between Threads
  4. 4Runtime Panic: BorrowMutError (RefCell)
  5. 5Which Smart Pointer Do I Need?
  6. 6Box<T>: Heap Allocation and Indirection
  7. 7Rc<T>: Shared Ownership (Single-Threaded)
  8. 8Rc<RefCell<T>>: Shared + Mutable (Single-Threaded)
  9. 9Breaking Reference Cycles with Weak
  10. 10Arc Without Mutex
  11. 11Further Reading
You Might Not Need a Smart PointerCompiler Error E0072: Recursive Type Has Infinite SizeCompiler Error E0277: Rc Cannot Be Sent Between ThreadsRuntime Panic: BorrowMutError (RefCell)Which Smart Pointer Do I Need?Box<T>: Heap Allocation and IndirectionRc<T>: Shared Ownership (Single-Threaded)Rc<RefCell<T>>: Shared + Mutable (Single-Threaded)Breaking Reference Cycles with WeakArc Without MutexFurther Reading

Rust's smart pointers are data structures that act like pointers but have additional metadata and capabilities. The ownership system handles most cases, but sometimes you need more flexibility.

Box<T> puts data on the heap. Rc<T> enables shared ownership. RefCell<T> provides interior mutability. Arc<T> is Rc for threads. Weak<T> breaks reference cycles.

This page focuses on when and how to use each smart pointer, and the errors you hit when you pick the wrong one.

Related Rust Topics
Rust Ownership & Borrowing: Move Semantics & Borrow RulesRust References & Lifetimes: Lifetime Annotations & ElisionRust Concurrency: Threads, Mutex, Channels & ArcRust Unsafe: Raw Pointers, FFI & Unsafe Blocks

Before reaching for smart pointers, exhaust simpler options:

  1. T, &T, &mut T — regular ownership and borrowing covers most cases
  2. Vec<T>, String — owned heap buffers with simple ownership
  3. Smart pointers — only when the ownership graph demands it

If you're wrapping everything in Box or Rc, step back and ask whether borrows or restructuring would work. The ownership and borrowing guide covers the borrow-first approach that eliminates most smart pointer needs.

Ready to practice?

Start practicing Rust Smart Pointers: Box, Rc, RefCell & Arc with spaced repetition

A type that directly contains itself has no finite size. Box adds indirection with a known pointer size:

// WRONG: infinite size
// enum List {
//     Cons(i32, List),  // error[E0072]: recursive type has infinite size
//     Nil,
// }

// RIGHT: Box adds indirection
enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

Rc uses non-atomic reference counting and does not implement Send:

// WRONG: Rc is not thread-safe
// use std::rc::Rc;
// let data = Rc::new(vec![1, 2, 3]);
// thread::spawn(move || {
//     println!("{data:?}");  // error[E0277]: Rc cannot be sent between threads
// });

// RIGHT: Arc uses atomic reference counting
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
    println!("{data_clone:?}");
}).join().unwrap();

Note: Arc<T> gives you shared ownership, not shared mutation. T must still be Send + Sync to be safely shared. For mutation, pair with Mutex or RwLock. The concurrency guide covers the Arc<Mutex<T>> and channel patterns in depth.

RefCell moves borrow checking from compile time to runtime. If you violate the rules (mutable borrow while immutably borrowed), it panics. Use scoped blocks to limit borrow lifetimes, or try_borrow_mut() for fallible borrowing.

RefCell checks borrow rules at runtime. Calling .borrow_mut() while a .borrow() is active panics:

use std::cell::RefCell;

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

// WRONG: immutable borrow still active when we try to mutate
// let borrowed = data.borrow();
// data.borrow_mut().push(4);  // panic: already borrowed: BorrowMutError

// RIGHT: drop the immutable borrow first
{
    let borrowed = data.borrow();
    println!("{:?}", *borrowed);
}  // borrowed drops here
data.borrow_mut().push(4);  // OK

// Or use try_borrow_mut for fallible borrowing (no panic)
match data.try_borrow_mut() {
    Ok(mut guard) => guard.push(5),
    Err(_) => eprintln!("already borrowed"),
}

Box for single ownership on the heap, Rc for shared ownership (single-threaded), Arc for shared ownership (multi-threaded), RefCell for interior mutability. If regular &T/&mut T works, you don't need a smart pointer.

SituationUse
Single owner, heap allocation neededBox<T>
Recursive type (tree, linked list)Box<T>
Multiple owners, single-threadedRc<T>
Multiple owners, multi-threadedArc<T>
Mutation through shared/immutable handle (single-threaded)RefCell<T>
Shared + mutable, single-threadedRc<RefCell<T>>
Shared + mutable, multi-threadedArc<Mutex<T>> or Arc<RwLock<T>>
Back-pointer / parent link (break cycles)Weak<T>

RefCell<T> is for interior mutability — mutating through an immutable reference, with runtime borrow checks. If you have single ownership and can use &mut T, you don't need RefCell.

// Put data on the heap
let b = Box::new(5);
println!("{}", *b);  // dereference with *

// Box for trait objects
let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];

Box has zero runtime cost beyond heap allocation — no reference counting, no runtime checks.

use std::rc::Rc;

let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);  // both a and b own the data
let c = Rc::clone(&a);

println!("count: {}", Rc::strong_count(&a));  // 3
drop(c);
println!("count: {}", Rc::strong_count(&a));  // 2

Rc::clone is cheap — it increments a counter, not a deep copy. The data is dropped when the last Rc goes out of scope.

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

let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone = Rc::clone(&shared);
clone.borrow_mut().push(4);
println!("{:?}", shared.borrow()); // [1, 2, 3, 4]

Rc cycles prevent deallocation—neither count reaches zero. Use Weak for back-pointers (child→parent). Weak doesn't keep data alive; call .upgrade() to check if the data still exists.

Rc cycles prevent deallocation — neither value's count reaches zero. Use Weak for back-pointers:

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

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,     // Weak: doesn't keep parent alive
    children: RefCell<Vec<Rc<Node>>>, // Rc: keeps children alive
}

let parent = Rc::new(Node {
    value: 1,
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});

let child = Rc::new(Node {
    value: 2,
    parent: RefCell::new(Rc::downgrade(&parent)),
    children: RefCell::new(vec![]),
});
parent.children.borrow_mut().push(Rc::clone(&child));

// Upgrade Weak to access the parent (returns Option<Rc<Node>>)
if let Some(p) = child.parent.borrow().upgrade() {
    println!("parent value: {}", p.value);
}

println!("strong: {}, weak: {}",
    Rc::strong_count(&parent),  // 1
    Rc::weak_count(&parent),    // 1
);

Pattern: children hold Rc (keep child alive), parents hold Weak (don't prevent deallocation).

Arc provides shared ownership, not shared mutation. For read-only sharing, no Mutex needed:

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

let config = Arc::new(String::from("production"));
let mut handles = vec![];
for _ in 0..3 {
    let config = Arc::clone(&config);
    handles.push(thread::spawn(move || {
        println!("mode: {config}");  // read-only — no Mutex needed
    }));
}
for h in handles { h.join().unwrap(); }

For read-heavy patterns with occasional writes, use Arc<RwLock<T>> instead of Arc<Mutex<T>> — it allows concurrent readers.

When you have a unique Arc (no other clones), Arc::get_mut() gives &mut T without a lock. Arc::make_mut() provides clone-on-write semantics. Understanding references and lifetimes helps explain why Rc and Arc exist -- they solve cases where the borrow checker cannot statically prove a single owner.

  • The Rust Book: Smart Pointers — Box, Rc, RefCell, reference cycles
  • The Rust Book: Reference Cycles — Weak, tree patterns
  • std::rc::Rc — strong_count, weak_count, Weak API
  • std::sync::Arc — get_mut, make_mut, thread-safety requirements

When to Use Rust Smart Pointers: Box, Rc, RefCell & Arc

  • Use `Box<T>` for recursive types and trait objects.
  • Use `Rc<T>` when multiple parts of code need to read the same data (single-threaded).
  • Use `Arc<T>` for shared ownership across threads.
  • Use `RefCell<T>` for interior mutability (mutation through an immutable handle).
  • Use `Rc<RefCell<T>>` for shared mutable data in single-threaded code.
  • Use `Arc<Mutex<T>>` or `Arc<RwLock<T>>` for shared mutable data across threads.

Check Your Understanding: Rust Smart Pointers: Box, Rc, RefCell & Arc

Prompt

When would you use Rc<RefCell<T>> versus Arc<Mutex<T>>?

What a strong answer looks like

Rc<RefCell<T>> is for single-threaded shared mutable state—cheaper but not thread-safe. Arc<Mutex<T>> is for multi-threaded shared mutable state—thread-safe but with locking overhead.

What You'll Practice: Rust Smart Pointers: Box, Rc, RefCell & Arc

Allocate data on the heap with Box::new()Define recursive types using BoxShare ownership with Rc::clone() and Arc::clone()Use RefCell for interior mutability (borrow/borrow_mut)Use try_borrow_mut for fallible borrowingBreak reference cycles with Weak and upgrade()Choose between Rc/Arc and RefCell/Mutex/RwLockKnow when to use Arc without Mutex

Common Rust Smart Pointers: Box, Rc, RefCell & Arc Pitfalls

  • Symptom: `error[E0277]: Rc<T> cannot be sent between threads safely`. Why: `Rc` uses non-atomic reference counting and does not implement `Send`. Fix: Replace `Rc` with `Arc` for any data shared across threads.
  • Symptom: Thread panics with `already borrowed: BorrowMutError`. Why: `RefCell` enforces borrow rules at runtime, and you called `.borrow_mut()` while a `.borrow()` was still active. Fix: Restructure so the immutable borrow is dropped before you mutably borrow, or use scoped blocks to limit borrow lifetimes.
  • Symptom: Memory keeps growing, `Rc::strong_count()` never reaches zero. Why: Two `Rc` values point to each other, creating a reference cycle that can never be freed. Fix: Use `Rc::downgrade()` to create `Weak<T>` references for back-pointers or parent links.
  • Symptom: `error[E0072]: recursive type has infinite size`. Why: The type directly contains itself, so the compiler cannot calculate a finite size. Fix: Wrap the recursive field in `Box<T>` to add a level of indirection with a known pointer size.
  • Symptom: Littering `Box::new()`, `Rc::new()`, and `.clone()` everywhere. Why: Smart pointers add heap allocation and indirection overhead. Fix: Only reach for smart pointers when regular ownership or borrowing genuinely cannot express what you need — most code works fine with `&T` and `&mut T`.

Rust Smart Pointers: Box, Rc, RefCell & Arc FAQ

Why do recursive types need Box?

Without indirection, the compiler cannot determine the size of a recursive type. Box provides a fixed-size pointer to heap data, making the size known at compile time.

What happens if I violate RefCell borrow rules?

RefCell checks borrows at runtime. If you try to borrow mutably while already borrowed (or vice versa), it panics. Use `try_borrow()` and `try_borrow_mut()` for fallible borrowing.

How do I avoid reference cycles with Rc?

Use `Weak<T>` for references that should not keep data alive. Create weak references with `Rc::downgrade()` and upgrade to `Rc` with `.upgrade()` when needed.

What is the performance cost of Rc vs Box?

Box has zero runtime cost for ownership. Rc has reference counting overhead on clone and drop. Arc is more expensive than Rc due to atomic operations.

Can I use Arc without Mutex?

Yes, if you only need shared immutable access. Arc<T> allows multiple threads to read T. For mutation, pair with Mutex or RwLock. For read-heavy workloads, RwLock allows concurrent readers.

Does Arc make T thread-safe?

Arc provides shared ownership with atomic reference counting, but T must still be Send + Sync to be shared safely. Arc<T> is Send + Sync only when T is Send + Sync.

What is the Sync analog of RefCell?

RwLock<T> provides the same read/write separation as RefCell, but with thread-safe locking instead of runtime borrow checks. For read-heavy patterns across threads, use Arc<RwLock<T>>.

Rust Smart Pointers: Box, Rc, RefCell & Arc Syntax Quick Reference

Box allocation
let b = Box::new(5);
Box dereference
let x = *b;
Rc creation
let a = Rc::new(5);
Rc clone
let b = Rc::clone(&a);
Rc count
Rc::strong_count(&a)
RefCell creation
let cell = RefCell::new(5);
RefCell borrow
let val = cell.borrow();
RefCell borrow mut
*cell.borrow_mut() += 1;
Arc creation
let a = Arc::new(5);
try_borrow_mut
if let Ok(mut g) = cell.try_borrow_mut() {
    *g += 1;
}
Weak reference
let weak = Rc::downgrade(&rc);
Upgrade Weak
if let Some(rc) = weak.upgrade() {
    println!("{}", rc);
}

Rust Smart Pointers: Box, Rc, RefCell & Arc Sample Exercises

Example 1Difficulty: 1/5

Fill in the operator to dereference the Box and get the inner value.

*
Example 2Difficulty: 3/5

Fill in the smart pointer needed for recursive data structures.

Box
Example 3Difficulty: 3/5

Fill in the smart pointer for dynamic dispatch.

Box

+ 36 more exercises

Further Reading

  • Rust Newtype Pattern: Catch Unit Bugs at Compile Time18 min read

Start practicing Rust Smart Pointers: Box, Rc, RefCell & Arc

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.