A pointer is a general concept for a variable that contains an address in memory. This address refers to, or “points at”, some other data.
Smart Pointer on the other hand, are data structures that act like a pointer but also have additional metadata and capabilities. String
and vec<T>
are also smart pointers. Smart pointers are usually implemented using structs. Unlike ordinary structs, smart pointers implement the Deref
and Drop
traits. The Deref
trait allows an instance of smart pointer struct to behave like reference. The Drop
trait allows you to customize the code that’s run when an instance of the smart pointer goes out of scope.
The most common smart pointers in std library are -
Box<T>
for allocating values on the heap.Rc<T>
, a references counting type that enables multiple ownership.Ref<T>
andRefMut<T>
, accessed throughRefCell<T>
a type that enforces the borrowing rules at runtime instead of compile time.
USING Box<T>
to point to Data on the heap
Boxes allow you to store data on the heap rather than the stack. What remains on the stack is the pointer to the heap data.
We’ll use them in situations such as -
- 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
Using a Box<T>
to store data on the heap
fn main() {
let b = Box::new(5); // Store the value 5 on the heap
println!("b = {}", b); // Dereference the box to access the value
}
Box::new(5)
allocates memory on the heap to store the value 5.- The variable b is a
Box<i32>
that points to this heap-allocated value. - When you print
b
, Rust automatically dereferences the box to access the value inside.
Enabling recursive types with boxes
Rust needs to know the size of types at compile time, but recursive types (like linked lists or trees) have an indeterminate size. Box<T>
can be used to create recursive types by storing the recursive part on the heap.
enum List {
Cons(i32, Box<List>), // Recursive part is boxed
Nil, // Base case
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
Box<List>
allows the Cons
variant to hold another List
without causing infinite size issues as rust knows the size of a pointer and it doesn’t change based on the amount of data it’s pointing to.
The Cons
variant needs the size of an i32
plus the space to store the box’s pointer data. The Nil
variant stores no values, so it needs less space than the Cons
variant. We now know that any List
value will take up the size of an i32
plus the size of a box’s pointer data. By using a box
, we’ve broken the infinite, recursive chain, so the compiler can figure out the size it needs to store a List
value.
Boxes provide only the indirection and heap allocation; they don’t have any other special capabilities.They also don’t have the performance overhead that these special capabilities incur, so they can be useful in cases like the cons list where the indirection is the only feature we need.
The Box<T>
type is a smart pointer because it implements the Deref
trait, which allows Box<T>
values to be treated like references. When a Box<T>
value goes out of scope, the heap data that the box is pointing to is cleaned up as well because of the Drop
trait implementation.
Treating Smart Pointers Like Regular References with the Deref
Trait
Implementing the Deref
trait allows you to customize the behaviour of the dereference operator *
. By implementing Deref
lets us to write code that operates on references and use that code with smart pointers too.
References
The variable x
holds value 5
and the variable y
stores the address of x
. Therefore when we dereference y
we get the value stored at address x
i.e. 5.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
We can write it using Box<T>
too cause it implements the Deref
Trait. The main difference here is we store a copy of x in y which is stored in the heap and we can use the dereference operator to dereference it and get value of y.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Implementing MyBox<T>
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0;
}
}
-
MyBox<T>
:- This is a tuple struct that wraps a value of type
T
. It’s similar toBox<T>
, but it’s a custom implementation.
- This is a tuple struct that wraps a value of type
-
new
Constructor:- The
new
function creates an instance ofMyBox
by wrapping the provided valuex
.
- The
-
Deref
Implementation:- The
Deref
trait is implemented forMyBox<T>
. - The
type Target = T;
line specifies that the target type for dereferencing isT
. - The
deref
method returns a reference to the inner value (&self.0
).
- The
-
Dereferencing in
main
:*y
works becauseMyBox<T>
implements theDeref
trait. The*
operator calls thederef
method, which returns a reference to the inner value (5
in this case).
Implicit Deref Coercions with Functions and Methods
What is Deref Coercion?
Deref coercion is a convenience in Rust that automatically converts a reference to a type that implements the Deref
trait into a reference to another type. This happens when you pass a reference to a function or method, and the type of the reference doesn’t exactly match the parameter type expected by the function.
For example:
&String
can be automatically converted to&str
becauseString
implements theDeref
trait to return&str
.- Similarly,
&MyBox<String>
can be converted to&str
through a sequence ofderef
calls.
When you pass a reference to a function, Rust checks if the type of the reference implements the Deref
trait. If it does, Rust calls the deref
method as many times as necessary to convert the reference into the type expected by the function.
For example:
- If you pass
&MyBox<String>
to a function that expects&str
, Rust will:- Call
deref
on&MyBox<String>
to get&String
. - Call
deref
on&String
to get&str
.
- Call
Example: Deref Coercion in Action
Let’s use the MyBox<T>
type and the hello
function to demonstrate deref coercion.
1. Define MyBox<T>
and Implement Deref
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
Here, MyBox<T>
is a custom smart pointer that implements the Deref
trait. The deref
method returns a reference to the inner value.
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
m
is aMyBox<String>
containing the string"Rust"
.&m
is a reference toMyBox<String>
.- Rust calls
deref
on&MyBox<String>
, which returns&String
. - Rust calls
deref
on&String
, which returns&str
. - The
&str
matches the parameter type of thehello
function, so the function executes successfully.
If Rust didn’t have deref coercion, you would need to manually convert &MyBox<String>
into &str
using explicit dereferencing and slicing:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
*m
dereferencesMyBox<String>
intoString
.&(*m)[..]
takes a full slice of theString
to get&str
.
This is much harder to read and write compared to the version with deref coercion.
Why is Deref Coercion Useful?
-
Convenience:
- You don’t need to manually add
&
and*
operators when passing references or smart pointers to functions. - Code becomes cleaner and easier to read.
- You don’t need to manually add
-
Flexibility:
- You can write functions that accept
&str
and still pass&String
,&MyBox<String>
, or other types that implementDeref<Target = str>
.
- You can write functions that accept
-
No Runtime Cost:
- Deref coercion happens at compile time, so there’s no performance penalty.
Deref Coercion and Mutability
Rust does deref coercion when it finds types and trait implementations in three cases:
- From
&T
to&U
whenT: Deref<Target=U>
- From
&mut T
to&mut U
whenT: DerefMut<Target=U>
- From
&mut T
to&U
whenT: Deref<Target=U>
immutable references will never coerce to mutable references.
Running Code on Cleanup with the Drop Trait
The Drop
trait lets you customize what we wanna do with the smart pointer when it goes out of scope such as releasing resources.
- The
Drop
trait has a single method:fn drop(&mut self)
. - It is automatically called when an instance of the type is about to go out of scope.
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping resource: {}", self.name);
}
}
fn main() {
let _res = Resource { name: String::from("MyResource") };
println!("Resource created");
} // `_res` goes out of scope here, and `drop` is called automatically
Dropping a value early with std::mem::drop
std::mem::drop
is a function in Rust’s standard library that explicitly drops a value before it would naturally go out of scope. This is useful when you want to force resource cleanup at a specific point in your program.
- Moves the value into the function, consuming it.
- Calls the
Drop
trait (if implemented) for the value. - Ensures the value is deallocated and cleaned up immediately.
- Does not require the value to be mutable.
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping resource: {}", self.name);
}
}
fn main() {
let res = Resource { name: String::from("MyResource") };
println!("Resource created");
std::mem::drop(res); // Explicitly drop `res` here
println!("Resource manually dropped");
} // No automatic drop since `res` is already consumed
Resource created
Dropping resource: MyResource
Resource manually dropped
Rc<T>
, the Reference Counted Smart Pointer
Rc<T>
(Reference Counted Smart Pointer) is a type in Rust’s standard library that allows multiple ownership of a value using reference counting. It enables multiple parts of a program to share ownership of a value, and the value is only dropped when the last reference to it is gone.
- Reference Counting: It keeps track of how many
Rc
instances refer to the same value. - Shared Ownership: Multiple
Rc<T>
instances can own the same value. - Immutable Borrowing:
Rc<T>
only allows shared (&T
) access, meaning you cannot mutate the value directly. - Thread-Local:
Rc<T>
is not thread-safe. UseArc<T>
for multi-threading.
use std::rc::Rc;
fn main() {
let a = Rc::new(10); // Create a reference-counted integer
let b = Rc::clone(&a); // Increase reference count
let c = Rc::clone(&a); // Increase reference count
println!("a = {}, reference count = {}", a, Rc::strong_count(&a));
}