Jump to content

Smart Pointers

From JOHNWICK

Yesterday we covered Closures, why they are important and edge cases to watch when working with them. If you missed it check the link down below.

Closures Yesterday we covered Error Handling with ?, if you missed it i’d suggest you check it out first. medium.com

Rust’s ownership model is its secret sauce for memory safety, but sometimes you need more flexibility than raw ownership provides.

That’s where smart pointers come in, they’re like specialized tools that extend Rust’s ownership rules to handle complex scenarios. Unlike raw pointers (*const T or *mut T), which are unsafe and leave memory management up to you, smart pointers add safety guarantees and automatic cleanup.

They’re not just a fancy wrapper, they’re essential for writing clean, safe, and efficient code.

Let’s break down the key smart pointers in Rust—Box<T>, Rc<T>, Arc<T>, RefCell<T>, and Weak<T>—and we’ll see below how they solve problems you’re likely to encounter in actual projects. Box<T>

Imagine you’re building a file parser for a log-processing tool at work. Your parser needs to handle a recursive data structure, like a tree of log entries where each entry might point to child entries (e.g., nested error reports).

Rust’s stack has a fixed size, so a deeply nested tree could cause a stack overflow if you store it directly. Enter Box<T>, a smart pointer that allocates data on the heap and takes ownership of it.

In the code below is how Box<T> helps. Suppose you define a LogEntry struct:

enum LogEntry {
    Message(String),
    Group { name: String, children: Vec<Box<LogEntry>> },
}

In this case, Box<LogEntry> ensures each child LogEntry lives on the heap, preventing stack issues for deeply nested structures.

When the Box goes out of scope, it automatically frees the memory, so you don’t have to worry about manual cleanup.

This is perfect for scenarios like parsing JSON-like log files, where the structure can get arbitrarily complex.

In a real project, I’ve seen Box<T> used in a compiler’s abstract syntax tree (AST). Each node in the AST (like a function or variable declaration) was heap-allocated with Box to manage memory efficiently while keeping the recursive structure clean. Without Box, the compiler would either choke on large inputs or require unsafe code, which is a maintenance nightmare.

Rc<T>: Sharing Ownership Within a Single Thread

Now, let’s say you’re working on a desktop application with a GUI, like a task manager where multiple UI components need to access the same list of tasks. In Rust, ownership is strict, one owner per value, so how do you share that task list without copying it everywhere? Rc<T> (Reference Counted) is your answer. It lets multiple parts of your program share ownership of a value within a single thread, keeping track of how many references exist.

Think of a scenario where your task manager has a TaskList struct, and both the main window and a status bar need to read from it. You could use Rc<TaskList> like shown in the code below.

use std::rc::Rc;

struct TaskList {
    tasks: Vec<String>,
}

fn main() {
    let task_list = Rc::new(TaskList {
        tasks: vec!["Write report".to_string(), "Fix bug".to_string()],
    });

    let main_window = Rc::clone(&task_list);
    let status_bar = Rc::clone(&task_list);

    println!("Main window sees: {:?}", main_window.tasks);
    println!("Status bar sees: {:?}", status_bar.tasks);
}

You can run the code on Rust Playground (https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=73ce6e78bf0bf2c6368e918fccbd37d4). Rc::clone doesn’t copy the data, it just increments a reference count. When each Rc goes out of scope, the count decreases, and the data is freed only when the count hits zero.

This is great for scenarios where multiple parts of your program need read-only access to shared data, like a configuration object in a CLI tool or a shared cache in a single-threaded server.

I once worked on a text editor where Rc was used to share a syntax highlighting configuration across different editor components. Each component could access the config without duplicating it, keeping memory usage low and updates consistent. However, Rc<T> only works in single-threaded contexts, which brings us to the next tool.

Arc<T>: Sharing Ownership Across Threads What happens when your application goes multithreaded? Say you’re building a web server that processes requests concurrently, and multiple threads need access to a shared connection pool for a database.

Rc<T> won’t cut it—it’s not thread-safe. This is where Arc<T> (Atomic Reference Counted) comes in, designed for safe sharing across threads.

Below is an example. You’re writing a web server using a thread pool to handle requests. Each thread needs access to a database connection pool:

use std::sync::Arc;

struct ConnectionPool {
    connections: Vec<String>, // Simplified for example
}
fn main() {
    let pool = Arc::new(ConnectionPool {
        connections: vec!["conn1".to_string(), "conn2".to_string()],
    });
    let mut handles = vec![];
    for _ in 0..3 {
        let pool = Arc::clone(&pool);
        let handle = std::thread::spawn(move || {
            println!("Thread accessing pool: {:?}", pool.connections);
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
}

You can play around with the code on Rust Playground (https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3f22c3623ad7896761ddfba921878a30). Arc<T> works like Rc<T>, but it uses atomic operations to safely manage the reference count across threads.

In our case above, each thread gets its own Arc pointing to the same connection pool, and the pool is only dropped when all threads are done with it. This pattern is common in real-world servers (like those built with Actix or Tokio) where shared resources, like caches or connection pools, need to be accessed concurrently.

In a project I saw, Arc was used in a game server to share a global game state (like player positions) across worker threads handling client connections. It kept the state consistent and thread-safe without locking up the server with excessive synchronization.

RefCell<T>: Mutating Shared Data Safely

Sometimes, you need to mutate shared data, but Rust’s strict borrowing rules (one mutable reference or multiple immutable ones) can make this tricky. Let’s say you’re building a game, and multiple game objects need to update a shared Scoreboard.

You can’t just use Rc<T> because it only allows immutable access. This is where RefCell<T> comes in, it lets you mutate data inside an Rc<T> by enforcing borrowing rules at runtime.

Here’s how it might look in a game:

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

struct Scoreboard {
    score: u32,
}

fn main() {
    let scoreboard = Rc::new(RefCell::new(Scoreboard { score: 0 }));
    let player1 = Rc::clone(&scoreboard);
    let player2 = Rc::clone(&scoreboard);

    player1.borrow_mut().score += 10; // Player 1 scores
    player2.borrow_mut().score += 20; // Player 2 scores

    println!("Final score: {}", scoreboard.borrow().score); // Prints 30
}

You can play around with the code on Rust Playground (https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=55a1d837f1fce16c2903729875ffeb98). RefCell<T> tracks borrows dynamically. You use borrow() for immutable access and borrow_mut() for mutable access, and if you break Rust’s borrowing rules (e.g., try to borrow mutably twice), it panics at runtime. This is super useful in scenarios where you need shared mutable state in a single thread, like updating a UI component’s state or managing a shared resource in a simulation.

In have seen RefCell used in a single-threaded event loop for a desktop app. The app had a shared state for user preferences that multiple components could update, and Rc<RefCell<T>> made it possible to keep the code clean while allowing controlled mutations.

Weak<T>: Avoiding Reference Cycles

One problem with Rc<T> or Arc<T> is reference cycles, where two objects reference each other, preventing their memory from ever being freed. Imagine a company management tool where Employee structs reference their Department, and each Department references its Employees. If both use Rc<T>, you’ve got a cycle, and the memory leaks.

Weak<T> solves this by creating a non-owning reference to an Rc<T> or Arc<T>. It doesn’t increment the reference count, so it doesn’t prevent the data from being dropped. Below is a code snippet example of whot it works.

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

struct Employee {
    name: String,
    department: Option<Weak<RefCell<Department>>>,
}
struct Department {
    name: String,
    employees: Vec<Rc<RefCell<Employee>>>,
}
fn main() {
    let dept = Rc::new(RefCell::new(Department {
        name: "Engineering".to_string(),
        employees: vec![],
    }));
    let emp = Rc::new(RefCell::new(Employee {
        name: "Alice".to_string(),
        department: Some(Rc::downgrade(&dept)),
    }));
    dept.borrow_mut().employees.push(Rc::clone(&emp));
    // Check if department still exists
    if let Some(dept_weak) = emp.borrow().department.as_ref() {
        if let Some(dept_rc) = dept_weak.upgrade() {
            println!("Department: {}", dept_rc.borrow().name);
        }
    }
}

You can play around with the code on Rust Playground (https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=22b0abd28894c213835d4d7795167a36). Here, Employee holds a Weak reference to Department, so when all strong Rc references to Department are dropped, its memory can be freed. You use upgrade() to get an Option<Rc<T>>, which is None if the data has been dropped.

This pattern is common in tree-like structures or graphs, like a DOM in a browser-like application or a scene graph in a game engine. In most scenarios you will seeWeak used in graph-based data models i.e for visualization tools. Nodes in the graph could reference each other, but using Weak for back-references prevented memory leaks when parts of the graph were removed.

Combining Smart Pointers In most Rust scenarios, you often combine smart pointers to solve complex problems.

For example, in a multithreaded web server, you might use Arc<Mutex<T>> to share mutable data across threads (Arc for thread safety, Mutex for safe mutation). Or in a single-threaded app, you might use Rc<RefCell<T>> to share mutable state. Here’s a practical example combining Arc and Mutex for a logging system in a server:

use std::sync::{Arc, Mutex};
use std::thread;

struct Logger {
    logs: Vec<String>,
}
impl Logger {
    fn log(&mut self, message: &str) {
        self.logs.push(message.to_string());
    }
}
fn main() {
    let logger = Arc::new(Mutex::new(Logger { logs: vec![] }));
    let mut handles = vec![];
    for i in 0..3 {
        let logger = Arc::clone(&logger);
        let handle = thread::spawn(move || {
            let mut logger = logger.lock().unwrap();
            logger.log(&format!("Log from thread {}", i));
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Logs: {:?}", logger.lock().unwrap().logs);
}

You can play around with the code on Rust Playground (https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1547da7083e14f0686afbd97d90f34f0).

The logger allows multiple threads to safely append to a shared log. In a real server, this could be part of a telemetry system collecting metrics from concurrent workers. When to Use Each Smart Pointer

Below are simple things to look after when picking the right smart pointer based on your needs:

  • Use Box<T> when you need to store data on the heap, like for recursive types or large objects, e.g., a tree in a compiler or parser.
  • Use Rc<T> for shared ownership in a single thread, like sharing a config in a GUI app.
  • Use Arc<T> for shared ownership across threads, like a connection pool in a web server.
  • Use RefCell<T> with Rc<T> for shared mutable data in a single thread, like a scoreboard in a game.
  • Use Weak<T> to avoid reference cycles in complex data structures, like a graph in a visualization tool.

Common Cases and How To Avoid Them Smart pointers are powerful, but they come with side-effects especially if not user appropriately.

For example, RefCell<T> can panic if you violate borrowing rules at runtime, so always ensure your borrow patterns are sound (e.g., don’t hold a borrow_mut() while calling code that might borrow again).

With Rc<T> or Arc<T>, watch out for reference cycles, always consider whether a Weak<T> is needed. In multithreaded code, combine Arc<T> with synchronization primitives like Mutex or RwLock to avoid data races.

In a project, I once debugged a panic caused by a RefCell being borrowed mutably twice in a UI update loop. The fix was restructuring the code to ensure only one mutable borrow happened at a time, using temporary variables to hold intermediate results.

Read the full article here: https://medium.com/rustaceans/smart-pointers-267851ada691