DEV Community

Cover image for Rust Smart Pointers Explained: Ownership, Memory, and Safety
Leapcell
Leapcell

Posted on

1 1 1 1 2

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<T> is used to allocate values on the heap.
  • Rc<T> is a reference-counted type that allows multiple ownership of data.
  • RefCell<T> 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<T> 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<T> 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);
}
Enter fullscreen mode Exit fullscreen mode

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<T> 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));
}
Enter fullscreen mode Exit fullscreen mode

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<T> is required.

Rc - Reference Counted

Rc<T> 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<T> is not thread-safe and cannot be used in multithreaded environments.

Common use cases for Rc<T> include:

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

Here's an example demonstrating how to use Rc<T> 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);
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • Rc::new is used to create a new instance of Rc<T>.
  • 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<T> is not safe for concurrent use across multiple threads. To address this, Rust provides Arc<T>.

Arc - Atomically Reference Counted

Arc<T> is the thread-safe variant of Rc<T>. 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<T> include:

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

Here's an example demonstrating how to use Arc<T> 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();
}
Enter fullscreen mode Exit fullscreen mode

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

Common use cases for Weak<T> 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<T> and Weak<T> 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());
}
Enter fullscreen mode Exit fullscreen mode

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

Key points about UnsafeCell<T>:

  • 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<T>:

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() });
}
Enter fullscreen mode Exit fullscreen mode

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<T> bypasses Rust's safety guarantees, it should be used with caution. In most cases, prefer Cell<T> or RefCell<T> for safe interior mutability.

Cell

Cell<T> is a type that enables interior mutability in Rust. It allows you to modify data even when you have an immutable reference. However, Cell<T> 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<T>:

  • 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<T>:

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());
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • Cell::new creates a new Cell<T> 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<T> 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<T>.

RefCell

RefCell<T> is another type that enables interior mutability, but it works for non-Copy types. Unlike Cell<T>, RefCell<T> 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<T> will panic at runtime.

Common Use Cases for RefCell<T>:

  • 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<T>:

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());
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • RefCell::new creates a new RefCell<T> 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<T> 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.
Enter fullscreen mode Exit fullscreen mode

Therefore, while RefCell<T> 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<T>
Rc<T>
Arc<T>
Weak<T> ✅ (weak ownership)
Cell<T> ✅ (Copy types only)
RefCell<T>
UnsafeCell<T>

Choosing the Right Smart Pointer

  • Use Box<T> when you need heap allocation with single ownership.
  • Use Rc<T> when you need multiple ownership in a single-threaded context.
  • Use Arc<T> when you need multiple ownership across multiple threads.
  • Use Weak<T> to prevent reference cycles with Rc<T> or Arc<T>.
  • Use Cell<T> for Copy types where interior mutability is needed.
  • Use RefCell<T> for non-Copy types where interior mutability is required.
  • Use UnsafeCell<T> 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

Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay