What Are Smart Pointers in Rust?

Smart pointers in Rust are types that own data, implement custom behavior via traits, and manage memory automatically.

When references run out of steam

You are building a parser for a custom configuration format. You need a Config struct that holds a list of settings. The settings arrive one by one from a file, and you don't know how many there will be until you finish reading. You try to put the settings directly inside Config. The compiler rejects you. Config needs a fixed size so the stack can allocate it, but the list of settings grows dynamically. You can't put a variable-sized blob inside a fixed-size struct.

You reach for a reference. You change Config to hold a &[Setting]. The compiler rejects you again. The reference doesn't own the data. If the temporary buffer you read the settings into goes out of scope, the reference dangles. Rust won't let you create a reference that outlives the data it points to.

You need something that points to data on the heap, owns that data, and cleans it up when you are done. You need a smart pointer.

Smart pointers are just structs

The term "smart pointer" comes from C++. In Rust, it is a misnomer that stuck. Smart pointers are not a special language feature. They are not magic keywords. They are structs.

Box<T> is a struct. Rc<T> is a struct. Vec<T> is a struct. They happen to hold a pointer to heap memory, but they are values just like i32 or String. You can pass them by value, move them, store them in other structs, and drop them.

This distinction matters because it means you can create your own smart pointers. You do not need compiler support. You need a struct with a pointer field and two trait implementations. If you understand structs and traits, you understand smart pointers.

The community treats smart pointers as a category because they solve a specific class of problems: managing heap data with automatic cleanup and ergonomic access. The category exists for convenience, not because the language treats them differently.

Treat smart pointers like any other struct. If you need custom behavior, write a struct.

The two traits that make them smart

Every smart pointer in Rust implements two traits. These traits give smart pointers their name and their power.

Deref lets the smart pointer behave like a reference. When you call a method on a smart pointer, Rust automatically dereferences it to find the method on the inner value. This is called deref coercion. It allows you to write box.len() instead of (*box).len().

Drop lets the smart pointer clean up resources when it goes out of scope. When a smart pointer is dropped, Rust calls its drop method. The Box implementation frees the heap memory. The Vec implementation frees the buffer and drops each element. The Rc implementation decrements a reference count and frees the data if the count hits zero.

You can implement Drop on your own structs to run custom cleanup code. This is how Rust handles file handles, network connections, and locks. The cleanup runs deterministically when the value goes out of scope. No garbage collector. No finalizers that run at unpredictable times.

Drop runs in reverse declaration order. If you have variables a, b, and c, they drop as c, b, a. This order matters when your cleanup code depends on other resources. Plan your variable declarations carefully if you implement Drop.

Trust the drop order. If your cleanup panics, Rust aborts the process to avoid double-free errors. Write robust Drop implementations that never panic.

Deref: Acting like a reference

Deref coercion is the feature that makes smart pointers feel natural. Without it, every method call would require explicit dereferencing. The syntax would be noisy and hard to read.

use std::rc::Rc;

fn main() {
    // Rc wraps a String on the heap.
    let data = Rc::new(String::from("Hello"));

    // String has a len() method. Rc does not.
    // Rust inserts a deref automatically.
    // This calls Rc::deref(&data) to get &String, then String::len.
    println!("Length: {}", data.len());

    // Deref coercion also works for function arguments.
    // This function expects &String.
    // We pass &Rc<String>. Rust coerces it.
    print_string(&data);
}

fn print_string(s: &String) {
    println!("{}", s);
}

Deref coercion works for both immutable and mutable references. If you have &mut Box<T>, Rust can coerce it to &mut T. If you have &Box<T>, Rust can coerce it to &T. The coercion follows the pointer chain. If you have Box<Rc<String>>, Rust can dereference twice to get &String.

Convention aside: When cloning an Rc, write Rc::clone(&data) instead of data.clone(). Both compile and both work. The explicit form signals to readers that you are cloning the reference count, not the underlying data. data.clone() looks like a deep clone but performs a shallow clone. The explicit form prevents confusion.

Deref coercion has limits. It does not work for function calls that take ownership. If a function expects String, you cannot pass Box<String>. You must move the value out of the box explicitly. Deref coercion only applies to references.

Understand the coercion rules. They make code clean, but they can hide what is happening. When debugging, remember that data.method() might be calling a method on the wrapper, not the inner type.

Drop: Cleaning up automatically

Drop runs when a value goes out of scope. The compiler inserts the drop call at the end of the scope. You never call drop manually in normal code. If you need to drop a value early, use the drop() function from the standard library. This function takes ownership and drops the value immediately.

struct Resource {
    name: String,
}

impl Drop for Resource {
    fn drop(&mut self) {
        // Custom cleanup runs here.
        println!("Cleaning up {}", self.name);
    }
}

fn main() {
    let r = Resource {
        name: String::from("FileHandle"),
    };

    // r is still valid here.
    println!("Using {}", r.name);

    // Drop r early.
    drop(r);

    // r is moved. This would be E0382.
    // println!("{}", r.name);
}

The drop method takes &mut self. This allows you to mutate the value during cleanup. You cannot move fields out of self in drop because self is a mutable reference, not an owned value. If you need to move data during cleanup, restructure your type.

Smart pointers use Drop to free heap memory. Box allocates memory with the global allocator and frees it in drop. Vec does the same. Rc and Arc manage reference counts. When the last Rc is dropped, the drop implementation frees the data.

Drop is deterministic. Resources are released exactly when the value goes out of scope. This makes resource management predictable. You can reason about when files close, when locks release, and when memory frees.

Write Drop implementations that are safe to call multiple times. Rust guarantees drop runs once, but defensive code helps. Never panic in drop. A panic during drop aborts the process.

Realistic example: Breaking infinite cycles

Recursive data structures are a common use case for smart pointers. A linked list node contains a pointer to the next node. If you store the next node directly, the size is infinite. The compiler cannot allocate an infinite struct.

enum List {
    // Cons holds a value and a pointer to the rest of the list.
    // Box breaks the infinite size cycle.
    // The Box puts the next node on the heap.
    // The Cons variant holds a fixed-size pointer.
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    // Build a list: 1 -> 2 -> 3 -> Nil
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));

    // Print the list recursively.
    print_list(&list);
}

fn print_list(list: &List) {
    match list {
        List::Cons(value, next) => {
            println!("{}", value);
            print_list(next);
        }
        List::Nil => {
            println!("End of list");
        }
    }
}

The Box in Cons puts the next node on the heap. The Cons variant holds a pointer to that node. The pointer has a fixed size. The compiler can calculate the size of List. The recursion works.

This pattern appears in syntax trees, graphs, and any structure where a node references other nodes of the same type. Without a smart pointer, you cannot represent these structures in Rust.

Use Box for recursive types. It is the standard solution. The heap allocation cost is small compared to the benefit of a workable data structure.

Pitfalls and compiler errors

Smart pointers move data. Box::new takes ownership of the value. If you try to use the value after moving it, the compiler rejects you.

fn main() {
    let x = String::from("Hello");
    let b = Box::new(x);

    // x is moved into Box.
    // This line causes E0382: use of moved value.
    // println!("{}", x);
}

The error E0382 tells you that x was moved. Smart pointers follow the same ownership rules as any other type. If you need to keep the original value, clone it before moving.

Deref coercion can hide type mismatches. If you have a function that expects &String and you pass &Rc<String>, Rust coerces the type. This is usually what you want. Occasionally, you need the wrapper type. If a function expects &Rc<String>, passing &String fails. You must wrap the value explicitly.

fn takes_rc(r: &Rc<String>) {
    println!("{}", r);
}

fn main() {
    let s = String::from("Hello");
    // This fails. s is a String, not an Rc.
    // takes_rc(&s);

    // Wrap in Rc.
    let r = Rc::new(s);
    takes_rc(&r);
}

Smart pointers have overhead. Box adds a heap allocation. Rc adds a reference count and a heap allocation. Arc adds an atomic reference count. The overhead is usually negligible. If you are in a performance-critical loop, measure the cost. References are faster than smart pointers. Use references when possible.

Drop order can cause issues if you are not careful. If variable a depends on variable b, and a drops after b, a's drop code might access invalid data. Declare variables in the correct order. b should be declared before a so b drops after a.

Plan your drop order. If the order is complex, use a helper struct to manage the lifecycle.

Vec is a smart pointer too

Vec<T> is a smart pointer. It implements Deref<[T]>. This is why you can call slice methods on a Vec. It implements Drop. This is why the buffer is freed when the Vec goes out of scope. It holds a pointer to heap memory.

You use Vec every day. It is the most common smart pointer in Rust. Recognizing this helps you understand the pattern. Vec wraps a pointer, a length, and a capacity. It manages the heap buffer. It provides ergonomic access through Deref.

Other standard library types are smart pointers too. String is a smart pointer to a UTF-8 byte buffer. Arc<T> is a smart pointer with atomic reference counting. Mutex<T> is a smart pointer that guards data with a lock.

The pattern is everywhere. A struct that wraps a pointer, implements Deref, and implements Drop. When you see this pattern, you are looking at a smart pointer.

Recognize the pattern. It simplifies the mental model. Smart pointers are not exotic. They are standard library structs that follow a convention.

Decision: Which pointer fits your problem

Pick the smart pointer that matches your ownership shape. The compiler will force your hand if you lie.

Use Box<T> when you need to allocate data on the heap but have a single owner. Use Box<T> for recursive data structures to break infinite size cycles. Use Box<T> when you want to move a large value without copying it.

Use Vec<T> when you need a growable list of items. Use Vec<T> when you need to store a dynamic number of elements and access them by index. Use Vec<T> when you need to push and pop elements efficiently.

Use Rc<T> when you need multiple owners in a single-threaded context. Use Rc<T> for graph structures where nodes share references. Use Rc<T> when you want to share data without copying and do not need interior mutability.

Use Arc<T> when you need multiple owners across threads. Use Arc<T> for shared state in concurrent programs. Use Arc<T> when you need thread-safe reference counting.

Use &T when you just need to borrow data and do not care about ownership. Use &T when the data is owned elsewhere and you only need read access. Use &T for performance-critical code where allocation overhead matters.

Use Rc<RefCell<T>> when you need multiple owners and interior mutability in a single-threaded context. Use Rc<RefCell<T>> for shared mutable state in UI frameworks or interpreters.

Use Arc<Mutex<T>> when you need multiple owners and interior mutability across threads. Use Arc<Mutex<T>> for shared mutable state in concurrent servers.

Reach for plain references when lifetimes are simple. The unsafe alternative is rarely worth it.

Pick the pointer that matches your ownership shape. The compiler will force your hand if you lie.

Where to go next