Labs ICT
โญ Pro Login

Smart Pointers

Box, Rc, and RefCell for advanced ownership.

3 min read | Rust Tutorial
โญ

Want the full learning experience?

Get structured courses, certificates, projects, and instructor support with LabsICT Pro.

Explore Pro Courses

Smart Pointers in Rust

Smart pointers are data structures that act like pointers but have additional capabilities. Rust has several built-in smart pointers that help manage memory and ownership in different scenarios.

The most common ones are Box<T>, Rc<T>, and RefCell<T>. Each solves a specific problem that regular references can't handle.

fn main() {
    // Box stores data on the heap
    let boxed = Box::new(42);
    println!("Boxed value: {}", boxed);

    // Regular stack-allocated
    let regular = 42;
    println!("Regular value: {}", regular);
}

Box<T> for Heap Allocation

Box<T> allocates data on the heap instead of the stack. It's useful for large data, recursive types, or when you need owned data.

#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

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

    let large_array = Box::new([0; 1000]);
    println!("Array length: {}", large_array.len());
}

Rc<T> for Reference Counting

Rc<T> (Reference Counted) lets multiple owners share the same data. It's great for when you need shared ownership but not mutation.

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    let data2 = Rc::clone(&data);
    let data3 = Rc::clone(&data);

    println!("Data: {:?}", data);
    println!("Data2: {:?}", data2);
    println!("Data3: {:?}", data3);
    println!("Reference count: {}", Rc::strong_count(&data));
}

RefCell<T> for Interior Mutability

RefCell<T> allows you to mutate data even when there are immutable references to it. This is called interior mutability and is checked at runtime.

use std::cell::RefCell;

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

    // Borrow immutably
    println!("Data: {:?}", data.borrow());

    // Borrow mutably
    data.borrow_mut().push(4);
    println!("After push: {:?}", data.borrow());
}

The Deref Trait

Smart pointers implement the Deref trait, which lets them be used like references. This means you can call methods on them just like regular references.

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

fn main() {
    let x = MyBox::new(5);
    println!("Value: {}", *x);

    let name = MyBox::new(String::from("Hello"));
    hello(&name);
}

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

The Drop Trait

The Drop trait lets you run code when a value goes out of scope. This is how smart pointers clean up resources automatically.

struct CustomPointer {
    data: String,
}

impl Drop for CustomPointer {
    fn drop(&mut self) {
        println!("Dropping CustomPointer with data: {}", self.data);
    }
}

fn main() {
    let a = CustomPointer { data: "first".to_string() };
    let b = CustomPointer { data: "second".to_string() };
    println!("CustomPointers created");

    // Drop happens in reverse order
    drop(a);
    println!("a dropped explicitly");
}

When to Use Each Type

Use Box<T> for heap allocation and recursive types. Use Rc<T> for shared ownership without mutation. Use RefCell<T> for interior mutability. Combine Rc<RefCell<T>> for shared, mutable data.

These smart pointers are essential for building complex data structures and managing memory in Rust programs.

๐Ÿงช Quick Quiz

What smart pointer enables shared ownership?