Chapter 15. Pointers for normal developers
This section will review what C++, Rust and some others call "smart pointers." The easiest way to think of a smart pointer is like a normal pointer but with functionality meant to safe-guard the developer from making memory management mistakes. Some of those include double-free errors, memory leaks and dangling pointers.
In other words, think of smart pointers as pointers for dumb developers (all of us!).
All problems in computer science can be solved by another level of indirection. - David Wheeler
What is a pointer in Rust?
A pointer is a variable that stores the memory address of another variable. In Rust, pointers are a bit different than in other languages because of the ownership system. Rust has two types of pointers - smart pointers and one you already know, references.
Let's take a deeper look at smart pointers by building our own, first.
Creating a smart pointer from scratch
If you've programmed in C++ or other languages you've probably come across smart pointers. If not, simply think of a smart pointer as a construct to act like a pointer - something that points to data somewhere else. That said, I say act because some smart pointers in Rust own their data!
But before diving into what's available, let's build one so you can get the gist of what's happening under the hood. What we're about to build will be an extremely basic version of the Box<T> smart pointer.
Our own smart pointer
We'll first define the struct and then add an impl block for the new method that returns our smart pointer. Let's call it StackBox.
struct StackBox<T>(T);
impl<T> StackBox<T> {
fn new(x: T) -> StackBox<T> {
StackBox(x)
}
}
This can hold data of any type T!
Next we'll need to implement a few traits so that this actually behaves like a pointer.
The Deref Trait
Before we implement anything, what on earth is Deref? Simply, Deref is the trait gives a type the ability to dereference a pointer. Here's what dereferencing a traditional pointer looks like:
fn main() {
let my_value = "Hello, world!";
let my_reference = &my_value;
assert_eq!("Hello, world!", my_value);
// here we use * to dereference my_reference
assert_eq!("Hello, world!", *my_reference);
}
When we implement our custom pointer we'll need to implement the Deref trait so that the compiler knows that our smart pointer type has that capability. To get going, we need to import the trait from std::ops::Deref.
FYI: we use an associated type
DataType = Tbelow; knowing the details for this isn't important!
use std::ops::Deref;
impl<T> Deref for StackBox<T> {
type DataType = T;
fn deref(&self) -> &Self::DataType {
&self.0
}
}
Importantly we use the tuple struct access for the first value with &self.0 to return the first value of the StackBox struct when we use the * dereference operator.
Here's what we have in total:
use std::ops::Deref;
struct StackBox<T>(T);
impl<T> StackBox<T> {
fn new(x: T) -> StackBox<T> {
StackBox(x)
}
}
impl<T> Deref for StackBox<T> {
type DataType = T;
fn deref(&self) -> &Self::DataType {
&self.0
}
}
Now let's use it.
fn main() {
let my_value = "Hello, world!";
let my_reference = MyStackBox::new(my_value);
assert_eq("Hello, world!", my_value);
assert_eq("Hello, world!", *my_reference);
}
Mutable dereferencing
We can also use the DerefMut trait to overload the * operator for mutable
references. Remember that we always have to abide by Rust's borrowing rules.
For mutable references we implement the DerefMut<TargetType=U> to go from &mut T to &mut U. Going from &mut T to &U uses the regular Deref trait because we can guarantee that there's only one reference to the data in &U. Importantly, we cannot go the other way - from a immutable reference to a mutable reference. This is because borrowing rules do not guarantee that the immutable reference is the only immutable reference to the data.
Let's quickly add this trait for our StackBox smart pointer.
use std::ops::Deref;
struct StackBox<T>(T);
impl<T> StackBox<T> {
fn new(x: T) -> StackBox<T> {
StackBox(x)
}
}
impl<T> Deref for StackBox<T> {
type DataType = T;
fn deref(&self) -> &Self::DataType {
&self.0
}
}
impl<T> DerefMut for StackBox<T> {
type DataType = T;
fn deref(&self) -> &Self::DataType {
&self.0
}
}
And we can now use mutable references:
fn main() {
let mut my_value = "Hello, world!";
let my_reference = MyStackBox::new(my_value);
assert_eq("Hello, world!", my_value);
assert_eq("Hello, world!", *my_reference);
}
The Drop Trait
Many languages have the concept of destructuring, where code is automatically
cleaned up after doing something. Rust is no different - we implement this behavior
using the Drop trait. This is critical to smart pointers because we often need
to run some custom cleanup to guarantee resources are freed.
For example, consider a database connection - it's critical to guarantee its successful disconnection and, for many databases, to check consistency and roll-back to a prior data checkpoint if something's gone wrong.
FXTradePointer
To see this in action, let's make ourselves a custom pointer that holds a trade between two currencies. In modeling a trade, it's critical that we record the trade after it's executed.
Let's add this in the drop method when we implement the Drop trait.
struct FXTradePointer {
base: String,
term: String,
price: String,
timestamp: String
}
impl Drop for FXTradePointer {
fn drop(&mut self) {
println!("{}{} trade executed at {} for a price of {}",
self.base,
self.term,
self.timestamp,
self.price
);
}
}
Let's make a currency pair and record the trade.
fn main() {
let trade_01 = FXTradePointer {
base: String::from("USD"),
term: String::from("CAD"),
timestamp: String::from("2025-04-16T15:12:17+00:00"),
price: String::from("1.3891")
};
println!("Executing trade {}{}", trade_01.base, trade_01.term);
}
You'll see output that looks like:
Executing trade USDCAD
USDCAD trade executed at 2025-04-16T15:12:17+00:00 for a price of 1.3891
The Box<T> smart pointer
Now since you've had some experience with smart pointers, having built a simple one, let's look at one of the most useful standard library smart pointers Box<T>.
With Box<T> we can allocate a pointer on the stack any type T stored on the heap, which let's us have dynamically sized or deeply nested data. The cost associated with this is the allocation of T's data on the heap - in some cases well worth it. After allocating a Box<T> smart pointer, we can then dereference that data using * which we're used to, because Box<T> implements the Deref trait`.
Before seeing a few specific capabilities of Box<T>, let's instantiate and derefence a string for fun.
fn main() {
let my_string_box = Box::new(String::from("I like boxes"));
println!("{}", my_string_box);
let my_string = *my_string_box;
println!("{}", my_string);
// println!("{}", my_string_box);
}
Notice how we were able to reference the my_string_box directly within the println! macro.
Try uncommenting the code in the last println! statement to see how ownership of the data inside box is transfered to my_string. You'll get an error that looks like:
error[E0382]: borrow of moved value: `my_string_box`
--> src/main.rs:10:24
|
7 | let my_string = *my_string_box;
| -------------- value moved here
...
10 | println!("{}", my_string_box);
| ^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `*my_string_box` has type `String`, which does not implement the `Copy` trait
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
More specifics
By default you can't clone a Box<T> - you'd need to add the T: Clone trait for that to work. You can do this by adding a #[derive(Clone)] directive to your type T stored in the Box<T>.
Data pointed to by a Box<T> are always dropped when that data goes out of scope or ownership is transfered. We saw this directly above when trying to use
the my_string_box after its ownership had been transferred to the dereferenced
my_string.
Box<T> smart pointers also gives us Rust developers extremely powerful polymorphism tools. Let's jump right into how we can do this in the next section.
We briefly touched on polymorphis when dealing with Vectors in chapter 8.
Trait objects, dynamic dispatch and polymorphism
Recall from Chapter 10 our discussion on traits. Specifically, we implemented
a custom Display trait for our beer-related types. We can actually use
custom traits like this as trait objects inside smart pointers like Box<T>.
This can give us extremely powerful runtime polymorphism capabilities. Let's get our hands dirty and see what exactly we're dealing with.
AdBehavior trait object
Let's revisit our DisplayAd from previous sections. We want to add some common behavior for all DisplayAd types that will generalize. Polymorphism is perfect for this objective.
Let's make ourselves an AdBehavior trait, implement it, then use it. First, the AdBehavior trait object. For usefulness we'll add a clone method and the
Debug trait so we can print things in debug.
trait AdBehavior: std::fmt::Debug {
fn title(&self) -> &str;
fn estimate_impressions(&self) -> u32;
fn box_clone(&self) -> Box<dyn AdBehavior>;
}
Speficially, notice the dyn keyword. This is a new keyword in Rust that indicates that the type is a trait object. This means that the type is not known at compile time, but will be determined at runtime.
This is important for polymorphism because it allows us to create a single type that can represent multiple types at runtime. We'll use this in our Box<T> instnce in a moment.
Implementing the AdBehavior trait
Next recall our DisplayAd struct:
#[derive(Debug, Clone)]
struct DisplayAd {
start_timestamp: i64,
budget: u32,
title: String,
copy: String,
call_to_action: String,
media_asset_urls: Vec<String>,
button_text: String,
target_url: String,
}
And now for our implementation. Like all implementations we
have to implement each function defined in the trait object - here's that's title, estimate_impressions and box_clone.
impl AdBehavior for DisplayAd {
fn title(&self) -> &str {
&self.title
}
fn estimate_impressions(&self) -> u32 {
(self.budget as f64 / 2.50 * 1000.0) as u32 // simple CPM logic
}
fn box_clone(&self) -> Box<dyn AdBehavior> {
Box::new(self.clone())
}
}
We're not quite ready to go yet - as mentioned before we need to explicitly enable cloning using the Clone trait on Box<T>.
Implementing Clone for Box<dyn AdBehavior>
Implementing Clone for Box<T> is a bit tricky because we need to implement the Clone trait for the Box<T> type itself. This is because Box<T> is a smart pointer that owns its data, and we need to ensure that the data is cloned correctly when we clone the Box<T>. For our purposes we'll just implement the Clone trait for Box<dyn AdBehavior>.
impl Clone for Box<dyn AdBehavior> {
fn clone(&self) -> Self {
self.box_clone()
}
}
Now let's finally use all this together.
Putting it all together
Below we'll create an ad and then clone it. We'll also print out the ad's title and estimated impressions, which now exist due to our AdBehavior trait object.
fn print_ad(ad: &Box<dyn AdBehavior>) {
println!("Ad: {:?}, Title: {}, Estimated Impressions: {}", ad, ad.title(), ad.estimate_impressions());
}
fn main() {
let display_ad = DisplayAd {
start_timestamp: 1721752441,
budget: 5000,
title: String::from("Summer festival season is here!"),
copy: String::from("Launch your ads in under 30 seconds!"),
call_to_action: String::from("Sell tix now"),
media_asset_urls: vec!["https://assets.b00st.com/ad1.png".into()],
button_text: String::from("Launch ads"),
target_url: String::from("https://b00st.com/app"),
};
let ad1: Box<dyn AdBehavior> = Box::new(display_ad);
let ad2 = ad1.clone();
print_ad(&ad1);
print_ad(&ad2);
}
Smart pointers + trait objects = runtime power
So what just happened?
- We made a trait that defines common behavior
AdBehavior. - We implemented it for our custom
DisplayAdtype. - We used
Box<dyn AdBehavior>to get runtime polymorphism. - We made it cloneable by manually implementing the
Clonetrait.
This lets us work with heterogeneous ad types in a single collection (like a Vec<Box<dyn AdBehavior>>), pass them around with shared behavior, and call methods on them-even though we don’t know the concrete type at compile time!
All the safety, none of the runtime risk (bugs). This is idiomatic, expressive Rust. Now let's look at how we can start to share data with other built-in smart pointers.
The Arc<T> and Rc<T> smart pointers
Arc and Rc accomplish basically the same thing: they allow you to share data across references. They accomplish this with reference counting with which you may be familar from other languages.
Let's start with Rc<T> and then jump into Arc<T>, with a quick summary at the end.
Basic reference counting
Given Rust's ownership rules things get tricky if we try to share data across multiple references. This is where Rc<T> plays a huge role. It allows our programs to share the same read-only data across multiple owners, and it keeps track of how many owners there are. When the last owner goes out of scope, the data is automatically cleaned up.
Though not thread-safe - it's intended for single-thread work - Rc<T> has a cousin that is thread-safe and uses essentially the exact same syntax. It works by keeping a reference count internally that tells the compiler how many owners have read access to the data.
Let's examine some code to quickly get moving and print some data's reference counts. Then we'll revisit our AdBehavior example to see how we can use Rc<T> to remove that pesky Clone trait implementation.
use std::rc::Rc;
fn main() {
let my_string = String::from("Hello, world!");
let my_string_ref = Rc::new(my_string);
println!("my_string_ref count: {}", Rc::strong_count(&my_string_ref));
let my_string_ref_2 = Rc::clone(&my_string_ref);
println!("my_string_ref: {}", my_string_ref);
println!("my_string_ref count: {}", Rc::strong_count(&my_string_ref));
{
let my_string_ref_3 = Rc::clone(&my_string_ref);
println!("my_string_ref count: {}", Rc::strong_count(&my_string_ref));
}
println!("my_string_ref count: {}", Rc::strong_count(&my_string_ref));
}
Look how that count changes as we clone the my_string_ref reference. As you've seen, the Rc::strong_count method returns the number of references to the data.
Before we move on to Arc<T> - Rc<T>'s thread-safe cousin - let's look at how we can use Rc<T> to remove the need for the Clone trait in our AdBehavior example.
Using Rc<T> with AdBehavior
We can use Rc<T> to remove the need for the Clone trait in our AdBehavior example. This is because Rc<T> allows us to share data across multiple owners without needing to clone it.
Let's keep the same definition for AdBehavior and DisplayAd, but remove the Clone trait implementation. We'll also remove the box_clone method from the AdBehavior trait.
use std::rc::Rc;
trait AdBehavior: std::fmt::Debug {
fn title(&self) -> &str;
fn estimate_impressions(&self) -> u32;
}
No more box_clone method!
#[derive(Debug)]
struct DisplayAd {
start_timestamp: i64,
budget: u32,
title: String,
copy: String,
call_to_action: String,
media_asset_urls: Vec<String>,
button_text: String,
target_url: String,
}
impl AdBehavior for DisplayAd {
fn title(&self) -> &str {
&self.title
}
fn estimate_impressions(&self) -> u32 {
(self.budget as f64 / 2.50 * 1000.0) as u32
}
}
Now we can use Rc<T> to create a reference counted pointer to our DisplayAd type. This allows us to share the same DisplayAd instance across multiple owners without needing to clone it.
fn print_ad(ad: &Rc<dyn AdBehavior>) {
println!(
"Ad: {:?}, Title: {}, Estimated Impressions: {}",
ad,
ad.title(),
ad.estimate_impressions()
);
}
fn main() {
let display_ad = DisplayAd {
start_timestamp: 1721752441,
budget: 5000,
title: String::from("Summer festival season is here!"),
copy: String::from("Launch your ads in under 30 seconds!"),
call_to_action: String::from("Sell tix now"),
media_asset_urls: vec!["https://assets.b00st.com/ad1.png".into()],
button_text: String::from("Launch ads"),
target_url: String::from("https://b00st.com/app"),
};
let shared_ad: Rc<dyn AdBehavior> = Rc::new(display_ad);
let a = Rc::clone(&shared_ad);
let b = Rc::clone(&shared_ad);
print_ad(&a);
print_ad(&b);
}
Much cleaner, no? Now let's look at doing this in a multi-threaded environment because if you were to try to use Rc<T> in a multi-threaded environment, you'd get a compile-time error. This is because Rc<T> cannot be shared across threads.
Multithreading and reference counting
Atomic reference counting - Arc<T> - smart pointers are great because they let us do the same thing as Rc<T>, but in a multi-threaded environment. This is because Arc<T> uses atomic operations to manage the reference count, which allows it to be shared across threads safely.
Arc<T> in action
We'll use the same simple example as we did for Rc<T> but use some actual threads to show how this works. We'll also use the Arc::strong_count method to show how many references there are to the data, similar to with Rc<T>.
use std::sync::Arc;
use std::thread;
fn main() {
let my_string = String::from("Hello, world!");
let shared = Arc::new(my_string);
println!("Initial ref count: {}", Arc::strong_count(&shared));
let mut handles = vec![];
for i in 0..5 {
let shared_ref = Arc::clone(&shared);
let handle = thread::spawn(move || {
println!("Thread {i}: {}", shared_ref);
println!("Thread {i} sees ref count: {}", Arc::strong_count(&shared_ref));
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final ref count: {}", Arc::strong_count(&shared));
}
Notice how we are able to effortlessly share the shared variable across multiple threads, thanks to the atomic operations underlying Arc<T>. This is a powerful feature of Rust that allows us to write safe, concurrent code without the need for locks or other synchronization primitives.
In summary
Here's a quick and dirty breakdown for you to remember the differences between Rc<T> and Arc<T> and when to use them.
- Thread safety:
Rcis not thread-safe, as its designed for single threaded scenarios.Arcis thread-safe, as it uses atomic operations to manage the reference count and can therefore be shared across multiple threads, safely.
- Performance:
Rcis slighly more performant thanArcbecause it does not need to use atomic operations to ensure thread safety.
- When to use:
- When you don't need multiple threads, use
Rc. - When you need multiple threads, use
Arc.