A pointer in programming is often a piece of data that directs to the location of another piece of data in memory. For example, your home address points to where you live.
Smart pointers are data structures that act like pointers but also have additional metadata and capabilities to manage memory automatically and safely.
Box smart pointers are one of the many smart pointers in Rust, it allows you to store data in the heap instead of the stack.
Here are some of their situations you might want to use a Box smart pointer:
- 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
Simple example of how to use a Box pointer
A box pointer type is denoted as Box<T> because it requires that you define the type that the box would hold. For example, the code below holds a Box of i32 numbers:
fn main(){
let numbers: Box<i32> = Box::new(5)
}
Now, instead of copying the i32 which was supposed to be stored in the stack, we can now store it in the heap and use its reference. Of course, you will want to use it for this purpose when you have a large amount of data you want to move around and you’d rather transfer ownership than waste memory copying it around.
Using Box with trait objects
Imagine you’re writing a function to interact with different types of data, but you don’t know exactly what types you’ll encounter beforehand. How can you design the function to be flexible and handle various data types?
Traits define a set of behaviors that different types can implement. This allows you to write code that works with any type that implements the trait, without knowing the specific type itself.
Now, let’s see how this works with an example. Imagine you have different animal types like cats, dogs, etc. You want a function to make any animal speak, regardless of its specific type
We’ll first create an Animal trait object like so:
pub trait Animal {
fn sound(&self)->String;
}
Then, we’ll create the different animal types and implement the Animal trait for all of them. Here is for Cat:
pub struct Cat;
impl Cat {
pub fn new( )-> Result<Cat, Error>{
Ok(Cat {})
}
}
impl Animal for Cat {
fn sound(&self)->String {
"Mew".to_string()
}
}
And also for Dog:
pub struct Dog;
impl Dog {
pub fn new( )-> Result<Dog, Error>{
Ok(Dog {})
}
}
impl Animal for Dog {
fn sound(&self)->String {
"bark".to_string()
}
}
And we should also have an error enum, in case the type doesn’t implement the Animal trait:
#[derive(Debug)]
pub enum Error {
UnknownError
}
And then finally, we’ll try to get the animals to make a sound. On the surface, you’d think the code should just work like the one below. We create a function that takes in the kind of animal and then it returns the sound the animal makes
fn animal_speak(kind: &str)-> Result<Animal, Error>{
if kind =="cat" {
Ok(Cat::new())
}else {
Err(animal::Error::UnknownError)
}
}
Fn main(){
let cat = animal_speak("cat").unwrap();
println!("{}", cat.sound(), );
}
Unfortunately, the above code will return an error like this one:
This error is due to the fact that we a’re trying to return a trait object Animal directly from the function animal_speak. Trait objects have a dynamic size, which means that their size isn’t known at compile time. Rust requires that the return type of functions be statically sized, but since Animal is not statically sized, Rust doesn’t allow you to return it directly.
So, to fix it, we’ll enclose trait in a Box Box<dyn Animal>
and introduce the dyn
keywork to explicitly declare that it’s a trait object with a dynamic size, so essentially, we are letting the compiler know that the function can return any type that implements then Animal trait in a Box. The code below will be our fix for it:
fn animal_speak(kind: &str) -> Result<Box<dyn Animal>, Error> {
match kind {
"cat" => Ok(Box::new(animal::Cat::new()?)),
"dog" => Ok(Box::new(animal::Dog::new()?)),
_ => Err(animal::Error::UnknownError),
}
}
and this will be the complete working code, you can test on your own in the Rust play ground:
#[derive(Debug)]
pub enum Error {
UnknownError
}
pub trait Animal {
fn sound(&self)->String;
}
pub struct Cat;
impl Cat {
pub fn new( )-> Result<Cat, Error>{
Ok(Cat {})
}
}
impl Animal for Cat {
fn sound(&self)->String {
"Mew".to_string()
}
}
pub struct Dog;
impl Dog {
pub fn new( )-> Result<Dog, Error>{
Ok(Dog {})
}
}
impl Animal for Dog {
fn sound(&self)->String {
"Bark".to_string()
}
}
fn animal_speak(kind: &str) -> Result<Box<dyn Animal>, Error> {
match kind {
"cat" => Ok(Box::new(Cat::new()?)),
"dog" => Ok(Box::new(Dog::new()?)),
_ => Err(Error::UnknownError),
}
}
fn main() {
let cat = animal_speak("cat").unwrap();
let dog = animal_speak("dog").unwrap();
println!("{}, {}", cat.sound(), dog.sound() );
}
Key notes:
- Box pointer owns the memory it points to, so, you’ll likely want to use it when you need to own the memory.
- When a Box goes out of scope, Rust automatically deallocates the memory on the heap.
- Box is used to store data when the size of the data is unknown at compile time or when the size of the data is too large to be stored on the stack.
- Heap allocation is also useful when you want to pass data between different threads.
To create a Box, you can use the Box::new()
function. This function takes the value that you want to store on the heap and returns a Box that points to the allocated memory. You can access the data stored in the Box by dereferencing it using the *
operator.
let x = Box::new(42);
println!("{}", *x); // prints 42
You can learn more about the Rust smart pointers in the documentation
Happy hacking!