Skip to content

Using Smart Pointers

RustC++
Box<T>unique_ptr<T>
Rc<T> and Arc<T>shared_ptr<T>
The above table outlines approximate analogs between smart pointer types in Rust and C++.

Put simply, a pointer is a variable that holds a memory address. In Rust the most common use of a pointer is when you prepend a variable with & to access it as a reference. Using this notation, you borrow the value the reference points to. These are effectively for performance because there's no cost to accessing that data (e.g. no copies).

A smart pointer is the same as a normal pointer, but is equipped with additional functionality. For example, both the String and Vec<T> types are smart pointer. These include:

  • Reference counting

References are pointers that only borrow data. Often smart pointers own the data they point to.

Typically smart pointers are structs that implement the Deref and Drop traits.

Using a Box to Point to Data on the Heap

The most basic smart pointer is the Box<T> type. They're handy in the follow situations:

  • When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size
  • When you have a large amount of data and you want to transfer ownership but ensure the data won’t be copied when you do so
  • When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type

At compile time, Rust needs to know how much space a type takes up.

To resolve this, when Rust doesn't know how large a type may be (potentially because the type is recursive), you can declare a pointer to that type such that it's allocated on the heap. This way, all that's on the stack is a pointer to a specific type. In Rust this is called indirection.

Indirection means that instead of storing a value directly you store a pointer to the value, thereby storing it indirectly.

Boxes provide only indirection and heap allocation.

The Deref Trait

Use the Deref trait, with your own implementation of deref(), to be able to dereference any custom type with the same syntax as regular pointers (using *).

The Drop Trait

Use the Drop trait, with your own implementation of drop() to call code on custom types when they goes out of scope. In this way, the Drop trait provides access to what is traditionally known as a destructor.

Since you can't disable the call to drop when a variable goes out of scope, one can also use std::mem::drop to force a value to be dropped before the end of its scope. This is useful when using locks so that other code can acquire the lock.

Using Reference Counted Smart Pointers

There are cases when a single value has multiple owners. A graph is a classic example whereby every node has several edges that connect to it, each edge in essence having some sort of shared ownership of the node.

The Rc<T> type is Rust's way of enabling shared ownership (reference counted). Rc<T> keeps track of the number of reference to it. Remember, Rc<T> is still allocated on the heap, this is because it can't be known at compile time which accessor will use it last.

Note: Rc<T> is for use in single-threaded applications, the atomic reference counted smart pointer Arc<T> can be shared between threads.

By convention, to create a clone of the data type you're interested in sharing use Rc::clone(&variable). Use Rc::strong_count(&variable) to get the number of counts for the reference.

Once the count reaches zero, Rc<T> will be cleaned up completely and dropped.

Note: Rc<T> is useful for reading immutable references in a shared way, but since the references aren't mutable thay can't be changed.

Pro Strats

Interior Mutability

Understanding how to use the interior mutability pattern can be somewhat confusing. Effectively, this pattern allows the underlying referenced variable (held in memory) to be altered, thus propogating the alteration to any pointers to that data. In languages other than Rust, writing code that uses interior mutability is fairly obvious: create a variable with some pointers/references and when you want to change the underlying value, simply dereference it. In these other languages the difficult part isn't figuring out how to change the underlying value, but instead how to protect the underlying value from being altered when it shouldn't be. Rust flips this proposition on it's head by disallowing the program to change the value without proper forethought about exactly where it should be changed. In Rust, interior mutability is achieved synchronously with Rc and RefCell and asynchronously with Arc and Mutex (respectively). With this syntax, the smart pointer (Rc/Arc) keep track of the data by providing methods to create pointers to the data (these methods should be fully qualified, e.g. Rc::clone(data)), while the locking mechanism (Mutex/RefCell) restrict access so that only one mutable reference can be active at any given time. While the Mutex syntax uses a more complicated API, the RefCell enables this functionality by using the borrow checker, simplfying the syntax required for the job.