Rust Smart Pointers Explained: Ownership, Memory, and Safety

What Are Smart Pointers in Rust? Smart pointers are a type of data structure that not only own data but also provide additional functionalities. They are an advanced form of pointers. A pointer is a general concept for a variable containing a memory address. This address "points to" or references some other data. References in Rust are denoted by the & symbol and borrow the values they point to. References only allow data access without providing any additional features. They also carry no extra overhead, which is why they are widely used in Rust. Smart pointers in Rust are a special kind of data structure. Unlike regular pointers, which merely borrow data, smart pointers typically own the data. They also offer additional functionalities. What Are Smart Pointers Used for in Rust and What Problems Do They Solve? Smart pointers provide powerful abstractions to help programmers manage memory and concurrency more safely and efficiently. Some of these abstractions include smart pointers and types that offer interior mutability. For example: Box is used to allocate values on the heap. Rc is a reference-counted type that allows multiple ownership of data. RefCell offers interior mutability, enabling multiple mutable references to the same data. These types are defined in the standard library and offer flexible memory management. A key characteristic of smart pointers is that they implement the Drop and Deref traits: The Drop trait provides the drop method, which is called when the smart pointer goes out of scope. The Deref trait allows for automatic dereferencing, meaning you don't need to manually dereference smart pointers in most situations. Common Smart Pointers in Rust Box Box is the simplest smart pointer. It allows you to allocate values on the heap and automatically frees the memory when it goes out of scope. Common use cases for Box include: Allocating memory for types with an unknown size at compile time, such as recursive types. Managing large data structures that you don't want to store on the stack, thereby avoiding stack overflow. Owning a value where you only care about its type, not the memory it occupies. For example, when passing a closure to a function. Here is a simple example: fn main() { let b = Box::new(5); println!("b = {}", b); } In this example, variable b holds a Box that points to the value 5 on the heap. The program prints b = 5. The data inside the box can be accessed as if it were stored on the stack. When b goes out of scope, Rust automatically releases both the stack-allocated box and the heap-allocated data. However, Box cannot be referenced by multiple owners simultaneously. For example: enum List { Cons(i32, Box), Nil, } use List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); } This code would result in the error: error[E0382]: use of moved value: a, because ownership of a has already been moved. To enable multiple ownership, Rc is required. Rc - Reference Counted Rc is a reference-counted smart pointer that enables multiple ownership of data. When the last owner goes out of scope, the data is automatically deallocated. However, Rc is not thread-safe and cannot be used in multithreaded environments. Common use cases for Rc include: Sharing data across multiple parts of a program, solving the ownership issues encountered with Box. Creating cyclic references together with Weak to avoid memory leaks. Here's an example demonstrating how to use Rc for data sharing: use std::rc::Rc; fn main() { let data = Rc::new(vec![1, 2, 3]); let data1 = data.clone(); let data2 = data.clone(); println!("data: {:?}", data); println!("data1: {:?}", data1); println!("data2: {:?}", data2); } In this example: Rc::new is used to create a new instance of Rc. The clone method is used to increment the reference count and create new pointers to the same value. When the last Rc pointer goes out of scope, the value is automatically deallocated. However, Rc is not safe for concurrent use across multiple threads. To address this, Rust provides Arc. Arc - Atomically Reference Counted Arc is the thread-safe variant of Rc. It allows multiple threads to share ownership of the same data. When the last reference goes out of scope, the data is deallocated. Common use cases for Arc include: Sharing data across multiple threads safely. Transferring data between threads. Here's an example demonstrating how to use Arc for data sharing across threads: use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(vec![1, 2, 3]); let data1 = Arc::clone(&data); let data2 = Arc::clone(&data); let handle1 = thread::spawn(move || { println!("data1: {:?}", data1); }); let handle2 = thread::spawn(move || {

Mar 14, 2025 - 21:01
 0
Rust Smart Pointers Explained: Ownership, Memory, and Safety

Cover

What Are Smart Pointers in Rust?

Smart pointers are a type of data structure that not only own data but also provide additional functionalities. They are an advanced form of pointers.

A pointer is a general concept for a variable containing a memory address. This address "points to" or references some other data. References in Rust are denoted by the & symbol and borrow the values they point to. References only allow data access without providing any additional features. They also carry no extra overhead, which is why they are widely used in Rust.

Smart pointers in Rust are a special kind of data structure. Unlike regular pointers, which merely borrow data, smart pointers typically own the data. They also offer additional functionalities.

What Are Smart Pointers Used for in Rust and What Problems Do They Solve?

Smart pointers provide powerful abstractions to help programmers manage memory and concurrency more safely and efficiently. Some of these abstractions include smart pointers and types that offer interior mutability. For example:

  • Box is used to allocate values on the heap.
  • Rc is a reference-counted type that allows multiple ownership of data.
  • RefCell offers interior mutability, enabling multiple mutable references to the same data.

These types are defined in the standard library and offer flexible memory management. A key characteristic of smart pointers is that they implement the Drop and Deref traits:

  • The Drop trait provides the drop method, which is called when the smart pointer goes out of scope.
  • The Deref trait allows for automatic dereferencing, meaning you don't need to manually dereference smart pointers in most situations.

Common Smart Pointers in Rust

Box

Box is the simplest smart pointer. It allows you to allocate values on the heap and automatically frees the memory when it goes out of scope.

Common use cases for Box include:

  • Allocating memory for types with an unknown size at compile time, such as recursive types.
  • Managing large data structures that you don't want to store on the stack, thereby avoiding stack overflow.
  • Owning a value where you only care about its type, not the memory it occupies. For example, when passing a closure to a function.

Here is a simple example:

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

In this example, variable b holds a Box that points to the value 5 on the heap. The program prints b = 5. The data inside the box can be accessed as if it were stored on the stack. When b goes out of scope, Rust automatically releases both the stack-allocated box and the heap-allocated data.

However, Box cannot be referenced by multiple owners simultaneously. For example:

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

This code would result in the error: error[E0382]: use of moved value: a, because ownership of a has already been moved. To enable multiple ownership, Rc is required.

Rc - Reference Counted

Rc is a reference-counted smart pointer that enables multiple ownership of data. When the last owner goes out of scope, the data is automatically deallocated. However, Rc is not thread-safe and cannot be used in multithreaded environments.

Common use cases for Rc include:

  • Sharing data across multiple parts of a program, solving the ownership issues encountered with Box.
  • Creating cyclic references together with Weak to avoid memory leaks.

Here's an example demonstrating how to use Rc for data sharing:

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    let data1 = data.clone();
    let data2 = data.clone();
    println!("data: {:?}", data);
    println!("data1: {:?}", data1);
    println!("data2: {:?}", data2);
}

In this example:

  • Rc::new is used to create a new instance of Rc.
  • The clone method is used to increment the reference count and create new pointers to the same value.
  • When the last Rc pointer goes out of scope, the value is automatically deallocated.

However, Rc is not safe for concurrent use across multiple threads. To address this, Rust provides Arc.

Arc - Atomically Reference Counted

Arc is the thread-safe variant of Rc. It allows multiple threads to share ownership of the same data. When the last reference goes out of scope, the data is deallocated.

Common use cases for Arc include:

  • Sharing data across multiple threads safely.
  • Transferring data between threads.

Here's an example demonstrating how to use Arc for data sharing across threads:

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

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

    let handle1 = thread::spawn(move || {
        println!("data1: {:?}", data1);
    });

    let handle2 = thread::spawn(move || {
        println!("data2: {:?}", data2);
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

In this example:

  • Arc::new creates a thread-safe reference-counted pointer.
  • Arc::clone is used to increment the reference count safely for multiple threads.
  • Each thread gets its own clone of the Arc, and when all references go out of scope, the data is deallocated.

Weak - Weak Reference Type

Weak is a weak reference type that can be used with Rc or Arc to create cyclic references. Unlike Rc and Arc, Weak does not increase the reference count, meaning it doesn't prevent data from being dropped.

Common use cases for Weak include:

  • Observing a value without affecting its lifecycle.
  • Breaking strong reference cycles to avoid memory leaks.

Here's an example demonstrating how to use Rc and Weak to create cyclic references:

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

struct Node {
    value: i32,
    next: Option<Rc<Node>>,
    prev: Option<Weak<Node>>,
}

fn main() {
    let first = Rc::new(Node { value: 1, next: None, prev: None });
    let second = Rc::new(Node { value: 2, next: None, prev: Some(Rc::downgrade(&first)) });
    first.next = Some(second.clone());
}

In this example:

  • Rc::downgrade is used to create a Weak reference.
  • The prev field holds a Weak reference, ensuring that it doesn't contribute to the reference count and thus preventing a memory leak.
  • When accessing a Weak reference, you can call .upgrade() to attempt to convert it back to an Rc. If the value has been deallocated, upgrade returns None.

UnsafeCell

UnsafeCell is a low-level type that allows you to modify data through an immutable reference. Unlike Cell and RefCell, UnsafeCell does not perform any runtime checks, making it a foundation for building other interior mutability types.

Key points about UnsafeCell:

  • It can lead to undefined behavior if used incorrectly.
  • It's typically used in low-level, performance-critical code, or when implementing custom types that require interior mutability.

Here's an example of how to use UnsafeCell:

use std::cell::UnsafeCell;

fn main() {
    let x = UnsafeCell::new(1);
    let y = &x;
    let z = &x;
    unsafe {
        *x.get() = 2;
        *y.get() = 3;
        *z.get() = 4;
    }
    println!("x: {}", unsafe { *x.get() });
}

In this example:

  • UnsafeCell::new creates a new UnsafeCell.
  • The .get() method provides a raw pointer, allowing modification of the data inside.
  • Modifications are performed inside an unsafe block, as Rust cannot guarantee memory safety.

Note: Since UnsafeCell bypasses Rust's safety guarantees, it should be used with caution. In most cases, prefer Cell or RefCell for safe interior mutability.

Cell

Cell is a type that enables interior mutability in Rust. It allows you to modify data even when you have an immutable reference. However, Cell only works with types that implement the Copy trait because it achieves interior mutability by copying values in and out.

Common Use Cases for Cell:

  • When you need to mutate data through an immutable reference.
  • When you have a struct that requires a mutable field, but the struct itself is not mutable.

Example of Using Cell:

use std::cell::Cell;

fn main() {
    let x = Cell::new(1);
    let y = &x;
    let z = &x;
    x.set(2);
    y.set(3);
    z.set(4);
    println!("x: {}", x.get());
}

In this example:

  • Cell::new creates a new Cell instance containing the value 1.
  • set is used to modify the internal value, even though the references y and z are immutable.
  • get is used to retrieve the value.

Because Cell uses copy semantics, it only works with types that implement the Copy trait. If you need interior mutability for non-Copy types (like Vec or custom structs), consider using RefCell.

RefCell

RefCell is another type that enables interior mutability, but it works for non-Copy types. Unlike Cell, RefCell enforces Rust's borrowing rules at runtime instead of compile-time.

  • It allows multiple immutable borrows or one mutable borrow.
  • If the borrowing rules are violated, RefCell will panic at runtime.

Common Use Cases for RefCell:

  • When you need to modify non-Copy types through immutable references.
  • When you need mutable fields inside a struct that should otherwise be immutable.

Example of Using RefCell:

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(vec![1, 2, 3]);
    let y = &x;
    let z = &x;
    x.borrow_mut().push(4);
    y.borrow_mut().push(5);
    z.borrow_mut().push(6);
    println!("x: {:?}", x.borrow());
}

In this example:

  • RefCell::new creates a new RefCell containing a vector.
  • borrow_mut() is used to obtain a mutable reference to the data, allowing mutation even through an immutable reference.
  • borrow() is used to obtain an immutable reference for reading.

Important Notes:

  1. Runtime Borrow Checking:

    Rust's usual borrowing rules are enforced at compile-time, but RefCell defers these checks to runtime. If you attempt to borrow mutably while an immutable borrow is still active, the program will panic.

  2. Avoiding Borrowing Conflicts:

    For example, the following code will panic at runtime:

let x = RefCell::new(5);
let y = x.borrow();
let z = x.borrow_mut(); // This will panic because `y` is still an active immutable borrow.

Therefore, while RefCell is flexible, you must be careful to avoid borrowing conflicts.

Summary of Key Smart Pointer Types

Smart Pointer Thread-Safe Allows Multiple Owners Interior Mutability Runtime Borrow Checking
Box
Rc
Arc
Weak ✅ (weak ownership)
Cell ✅ (Copy types only)
RefCell
UnsafeCell

Choosing the Right Smart Pointer

  • Use Box when you need heap allocation with single ownership.
  • Use Rc when you need multiple ownership in a single-threaded context.
  • Use Arc when you need multiple ownership across multiple threads.
  • Use Weak to prevent reference cycles with Rc or Arc.
  • Use Cell for Copy types where interior mutability is needed.
  • Use RefCell for non-Copy types where interior mutability is required.
  • Use UnsafeCell only in low-level, performance-critical scenarios where manual safety checks are acceptable.

We are Leapcell, your top choice for hosting Rust projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ

Read on our blog