Rust Smart Pointers Tutorial
In this Rust tutorial we learn about smart pointers that provide extra functionality beyond that of traditional pointers.
We also learn about referencing and dereferencing, the Box<T> smart pointer, and the Deref and Drop traits.
What is a pointer
When a program runs, it stores its data, like variables, in a random location in the system’s memory (the RAM). When the program is terminated, the data is removed from memory and it becomes available to the system again.
A pointer is simply the variable that stores the address of another variable in memory.
As an example, think of a gated community. Our friend John Doe has invited us to visit him and gave us the location of the gated community.
So, we know the general location of where he lives. Similarly, we know that temporary data is stored in memory, a general location for it.
We can go to the gated community, but if we want to visit our friend, we need his specific house address.
When we get to the security gate, we have to ask the guard in which house our friend with the name John Doe lives. The guard has stored the house address that correlates to our friend’s name. Similarly, if we want the address of a piece of temporary data, we need to find out where it is and store it.
Pointers allow us to do that, they point us to the address of our data in memory, much like the guard would point us to our friends house within the community.
What is a smart pointer
A smart pointer is a pointer that provides additional features beyond a traditional pointer, such as keeping track of the memory that the smart pointer points to. Smart pointers can also be used to manage other resources like network connections.
Smart pointers also provide additional functionality for references. A common example is “reference counted smart pointer type” which allows our data to have multiple owners by keeping track of them, cleaning up the data once it has no owner left. Smart pointers own the data they point to, while references only borrow the data.
If you’re familiar with the C++ language, you’ll feel right at home in this lesson as that is where smart pointers were first used.
Types of smart pointers
In this tutorial we’ll cover some of the following five smart pointers in Rust.
Smart Pointer | Description |
---|---|
Box<T> | This smart pointer points to the data allocated on the heap of type T. It's used to store data on the heap, rather than on the stack. |
Deref<T> | This smart pointer allows us to customize the behavior of the dereference operator. |
Drop<T> | This smart pointer frees the space allocated on the heap from memory when a variable goes out of scope. |
Rc<T> | This smart pointer is the reference counted pointer. It keeps a record of the number of references to a value that's stored on the heap. |
RefCell<T> | This smart pointer allows us to borrow mutable data even if the data is immutable, also known as interior mutability. |
How to use Box<T>
Box<T> is used to store data on the heap and not on the stack. The Box smart pointer itself will be stored on the stack, and the data it points to will be stored on the heap.
To create a Box smart pointer we use new() with the value we want to store on the heap between the parentheses.
Box::new(value_stored_on_heap);
fn main() {
let a = Box::new(5);
println!("The value of a is : {}", a);
}
In the example above, the variable a contains the value of Box which points to the data value 5.
The Box is stored on the stack, and the data it points to (5) is stored on the heap. When the program ends, the Box is deallocated from memory.
Other than storing data on the heap, a Box smart pointer doesn’t have any performance overhead.
How to use Deref<T>
Before we learn about Deref<T>, let’s take a look at what the dereferencing operator is and why it’s used.
We know that a pointer stores the address of another variable in memory. To get the memory address of a variable, we use the unary reference of (&) operator.
The reference operator is also known as the address of operator.
Once we have the memory address of the variable, we can get the value stored in that address. To do this we need to dereference the memory address with the unary dereferencing operator (*).
The dereference operator is also known as the indirection operator.
Simply put, the dereferencing operator allows us to get the value stored in the memory address of a pointer.
In Rust, we use the Deref
Let’s see how a typical referencing and dereferencing would work.
fn main() {
let a = 5;
let b = &a;
if a == *b {
println!("Equal");
} else {
println!("Not equal");
}
}
In the example above, a contains the value of 5 and b contains the reference of a.
When we use *b in the if statement, it represents the same value as a (5) and we can compare the two values.
If we change the if statement to &b, the compiler will raise an error.
fn main() {
let a = 5;
let b = &a;
if a == &b {
println!("Equal");
} else {
println!("Not equal");
}
}
The compiler will raise an error that they cannot be compared. That’s because & gets us the address of the variable in memory, not the actual value.
error[E0277]: can't compare `{integer}` with `&&{integer}`
We can also use Box<T> as a reference and the result will be the same.
fn main() {
let a = 5;
let b = Box::new(a);
if a == *b {
println!("Equal");
} else {
println!("Not equal");
}
}
In the example above, we use a Box instead of a typical reference. The only difference between this example and the previous, is that b contains the Box pointing to the data, rather than referring to the value by using the & operator.
To see how a smart pointer will behave differently from a regular reference, let’s create a custom smart pointer that’s similar to Box
// Custom smart pointer
struct CustomBox<T>(T);
impl<T> CustomBox<T> {
fn new(y : T) -> CustomBox<T> {
CustomBox(y)
}
}
fn main() {
let a = 5;
let b = CustomBox::new(a);
if a == *b {
println!("Equal");
} else {
println!("Not equal");
}
}
The smart pointer can be defined as a tuple struct. In the example above, we only have one element, CustomBox<T>. After we create the tuple struct, we define the new() function of type CustomBox<T>.
However, when we try to derefence the CustomBox, the compiler raises an error.
error[E0614]: type `CustomBox<{integer}>` cannot be dereferenced
--> src\main.rs:16:10
|
16 | if a == *b {
| ^^
Customized smart pointers, similar to Box<T> cannot be dereferenced and that’s where the Deref trait comes in to save the day.
Let’s implement the Deref trait, then do a step by step walkthrough.
// Custom smart pointer
struct CustomBox<T> {
a : T,
}
// Use Deref from standard library
use :: std::ops::Deref;
// Implement Deref
impl<T> Deref for CustomBox<T> {
type Target = T;
fn deref(&self) -> &T
{
&self.a
}
}
fn main() {
let b = CustomBox{ a : 5 };
// Now we can dereference b
println!("{}", *b.deref());
}
1. The Deref trait is defined in the standard library so we have to import it.
use :: std::ops::Deref;
2. Then we implement the Deref trait on the CustomBox where we implement the deref() method, which returns the reference of a.
impl<T> Deref for CustomBox<T> {
type Target = T;
fn deref(&self) -> &T
{
&self.a
}
}
Target is an associated type for the Deref trait and is the resulting type after dereferencing.
3. Finally, we create an instance of CustomBox into b, call the deref method with b.deref() and the reference which is returned is dereferenced with *.
fn main() {
let b = CustomBox{ a : 5 };
// Now we can dereference b
println!("{}", *b.deref());
}
How to use Drop
When we don’t use a resource anymore, we don’t want it to take up space in memory. The Drop trait will deallocate that space in memory that the pointer points to.
We don’t have to call the Drop trait explicitly, Rust automatically does it for us when a value goes out of scope.
struct Example {
a: i32
}
// when an instance of example
// goes out of scope, use this
// custom drop() method
impl Drop for Example {
fn drop(&mut self) {
println!("Custom Drop: Dropping the instance of Example: {}", self.a);
}
}
fn main() {
let var_1 = Example{a: 1};
let var_2 = Example{a: 5};
println!("Example instances created");
}
In the example above, we implement the Drop trait for the Example struct.
The Drop trait is used to implement the drop() method, which takes a mutable reference to itself.
In the drop() method we add some custom code which, in this case, prints a simple message to the console. The message will be printed when an instance of Example goes out of scope and is deallocated in memory.
In main() we create two instances of the Example struct and print a message to the console that they were created. When these instances go out of scope, the custom drop() method will trigger and it should print a message to the console.
Example instances created
Custom Drop: Dropping the instance of Example: 5
Custom Drop: Dropping the instance of Example: 1
The output shows us exactly what we expect. The instances are created, and when Rust is finished with main() they go out of scope and print a message to the console when they do.
Summary: Points to remember
- A pointer is a variable that stores the address of another variable in memory.
- A smart pointer is a pointer with extra features.
- Box<T> is used to store data on the heap and not on the stack.
- The Deref<T> trait allows us to customize the behaviour of the dereferencing operator.
- The Drop trait allows us to customize the behaviour of an instance’s deallocation.