I was struggling with Box, Ref, RefCell, Rc, Arc for a while when I decided that I need to implement each one of them to really understand them. To do this, I played with Box type on both primitive and non primitive data types. This article is all about my understanding of the Box
wrapper type along with an example for each.
All at the very basic level, Box
is a way of initializing values on the Heap. In other words, its a Smart Pointer. It’s like Initializing a value and having a pointer in C / C++ / Java ( Rust style, of course ). To initialize a Box type, we just use the Box::new(T)
associated type, and pass in any value that needs to be on the heap.
To understand Box type, we first need to know how to check the address of any variable. Before diving deeper, let’s take a look at printing memory addresses of variables. Refer to the following program
fn main() {
let i : i32 = 10 ;
let j = i ;
println!("Address of variable i is {:p}",&i);
println!("Address of variable j is {:p}",&j);
}
Let’s take a look at an example by defining an integer and try printing its address. Running this program will give us the address of the variable i.
Address of variable i is 0x7ffc4b21e3c0
Address of variable j is 0x7ffc4b21e3c4
Notice that the two types were primitive types. And we have two different values in memory. This is because primitive types implement **Copy**
trait. And these values were copied while creating new variables when we assigned j to i.
Let’s say we want to initialize the integer on the heap. To do this, we define a variable of type Box<i32>
and write the same program using the Box wrapper.
fn main() {
let i: Box<i32> = Box::new(10);
println!("Address of variable i is {:p}", i.as_ref());
let j = i;
println!("Address of variable j is {:p}", j.as_ref());
}
And the output of this program is
Address of variable i is 0x55d682129ba0
Address of variable j is 0x55d682129ba0
Notice how the addresses of the variable is exactly the same. This is because Box acts as a pointer to a variable. It wraps an object type of any size into a pointer to that object. In our case, an i32
type.
One small change to this program as compared to the original one with primitive types is that the assignment of variable i
to j
occurs after printing the address of i
. Try running the program without this .
fn main() {
let i : Box<i32> = Box::new(10) ;
let j = i ;
println!("Address of variable i is {:p}",i.as_ref());
println!("Address of variable j is {:p}",j.as_ref());
}
Upon running this program, we get the following error .
error[E0382]: borrow of moved value: `i`
--> src/main.rs:4:47
|
2 | let i : Box<i32> = Box::new(10) ;
| - move occurs because `i` has type `Box<i32>`, which does not implement the `Copy` trait
3 | let j = i ;
| - value moved here
4 | println!("Address of variable i is {:p}",i);
| ^ value borrowed here after move
|
= 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)
help: consider cloning the value if the performance cost is acceptable
|
3 | let j = i.clone() ;
| ++++++++
this error is quite clear as it tells us that Box Doesn’t implement the copy trait. As soon as we assigned i to j, we had a move. Hence, any attempt to access i
after the move will result in an error. As far as Box Pointers are concerned, they will always have the same ownership and borrowing rules as regular objects in Rust.
If we really wanted to share the Variable, we can clone
our box to generate a new variable which points to the same address in memory — as long as they are immutable references . Let’s try it out first.
fn main() {
let i : Box<i32> = Box::new(10) ;
let j = i.clone() ;
println!("Address of variable i is {:p} with pointer address {:p}",i.as_ref(),i);
println!("Address of variable j is {:p} with pointer address {:p}",j.as_ref(),j);
}
The output of this program is as expected. Underlying value pointer address is still the same but the box pointer address changes because of the clone.
Address of variable i is 0x55acdf605ba0 with pointer address 0x55acdf605ba0
Address of variable j is 0x55acdf605bc0 with pointer address 0x55acdf605bc0
Apart from acting as Smart pointers, Box also allows us to mutate variables using the *
operator. This is handy because we can directly operate on Box types instead of first unwrapping it. The following example mutates box value type and changes the underlying value. Notice how the addresses of the two variables change as soon as we define them as mutating variables.
fn main() {
let mut i : Box<i32> = Box::new(10) ;
let mut j = i.clone() ;
println!("Address of variable i before mutating is {:p}",i.as_ref());
println!("Address of variable j before mutating is {:p}",j.as_ref());
*i = 20;
println!("Address of variable after mutation i is {:p}",i.as_ref());
println!("Value of variable after mutation i is {}",i);
println!("Address of variable after mutation j is {:p}",j.as_ref());
println!("Value of variable after mutation j is {}",j);
}
Below is the output for this program.
Address of variable i before mutating is 0x55a9e2c02ba0
Address of variable j before mutating is 0x55a9e2c02bc0
Address of variable after mutation i is 0x55a9e2c02ba0
Value of variable after mutation i is 20
Address of variable after mutation j is 0x55a9e2c02bc0
Value of variable after mutation j is 10
One has to keep this into consideration. Unlike traditional languages where we could change the value of the underlying type just by dereferencing it from any number of pointer references , Box
doesn’t encourage this. Instead a clone will actually clone the value inside the Box type. Let’s take another example with a non primitive type to highlight this.
struct Book {
language: String
}
impl Book {
fn new( language : String) -> Self {
Book{language : language.to_owned()}
}
}
fn main() {
let cppProgrammingBook = Book::new("C++".to_owned());
let mut i = Box::new(cppProgrammingBook) ;
println!("Address of variable i before mutating is {:p}",i.as_ref());
println!("Value of variable before mutating i is {}",i.language);
}
Take a minute to read through this program and guess what happens ? Will this program print an address and the language C++
? If you answered yes, then you’re right. It takes in the value of the struct and prints the language. Now try cloning the variable i
and notice the error.
fn main() {
let cppProgrammingBook = Book::new("C++".to_owned());
let mut i = Box::new(cppProgrammingBook) ;
let j = i.clone();
println!("Address of variable i before mutating is {:p}",i.as_ref());
println!("Value of variable before mutating i is {}",i.language);
}
1 | struct Book {
| ----------- doesn't satisfy `Book: Clone`
...
15 | let j = i.clone();
| ^^^^^ method cannot be called on `Box<Book>` due to unsatisfied trait bounds
|
|
198 | / pub struct Box<
199 | | T: ?Sized,
200 | | #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
201 | | >(Unique<T>, A);
| |_- doesn't satisfy `Box<Book>: Clone`
|
= note: the following trait bounds were not satisfied:
`Book: Clone`
which is required by `Box<Book>: Clone`
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following trait defines an item `clone`, perhaps you need to implement it:
candidate #1: `Clone`
help: consider annotating `Book` with `#[derive(Clone)]`
This brings us to one important Conclusion. To clone a Box pointer, the underlying type must be cloneable. The compiler suggests us to use a clone it since we’re using mutable smart pointers. The same error wouldn’t occur if we didn’t make our variable mutable. Consider this case.
fn main() {
let cppProgrammingBook = Book::new("C++".to_owned());
let i = Box::new(cppProgrammingBook) ;
let j = i.clone();
let val =
println!("Address of variable i {:p}",i);
println!("Address of variable i {:p}",j);
}
In this case, we see both pointers point to the same different in memory.
Address of variable i 0x56473fcbcba0
Address of variable j 0x56473fcbcbe0
Another use case where Box comes in really handy is the use of Recursive types . A lot of data structures like Linked List, Trees, Graphs may a size that cannot be determined at compile time. In such casees we can use our Box type to allocate everything on heap, and keep references to those items as a Boxed type. Consider the example below from our Book struct. We now want to also include a field that holds a similar book to the current one.
#[derive(Clone)]
struct Book {
language: String,
similar : Option<Book>
}
impl Book {
fn new( language : String) -> Self {
Book{language : language.to_owned(), similar:Option::None}
}
}
fn main() {
let cpp_programming_book = Book::new("C++".to_owned());
let i = Box::new(cpp_programming_book) ;
println!("Address of variable i {:p}",i);
}
While working with any other language, this may seem completely normal. This code wouldn’t compile in Rust. This is the error that you may see.
--> src/main.rs:2:1
|
2 | struct Book {
| ^^^^^^^^^^^
3 | language: String,
4 | similar : Option<Book>
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
4 | similar : Option<Box<Book>>
| ++++
The compiler tries to get the exact size of the struct to initialize on our stack by default. By using a Book type within a Book Type, this can cause indefinite recursion. As programmers we know this cannot go on forever, but theoretically, it’s an infinite recursion. So, we see an error like this. In Such cases, we initialize our struct wrapping it up in a Box — something like this.
#[derive(Clone)]
struct Book {
language: String,
similar : Option<Box<Book>>
}
Notice how we initialized Option<Box<Book>> instead of Box<Option<Book>>
. Both of these are valid syntaxes.. We just save some space by not using a Box at all if there is no book similar to this one / the value is not known. This change should compile the program just fine because the compiler knows it needs a fixed size pointer to represent our similar
Book reference.
In summary, a box type should be used when.
Another important point to note is that as soon as we create a box pointer from an underlying struct, our Box wrapper type takes ownership of the underlying object. We cannot use the original reference from that point on. Try running the program below
fn main() {
let cppProgrammingBook = Book::new("C++".to_owned());
let i = Box::new(cppProgrammingBook) ;
let j = i.clone();
println!("Address of underlying variable {:p}", &cppProgrammingBook);
println!("Address of variable i {:p}",i);
println!("Address of variable i {:p}",j);
}
13 | let cppProgrammingBook = Book::new("C++".to_owned());
| ------------------ move occurs because `cppProgrammingBook` has type `Book`, which does not implement the `Copy` trait
14 | let i = Box::new(cppProgrammingBook) ;
| ------------------ value moved here
15 | let j = i.clone();
16 | println!("Address of underlying variable {:p}", &cppProgrammingBook);
| ^^^^^^^^^^^^^^^^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
As you can see, compiler suggests the use of cloning an entire struct if the performance cost is acceptable. In real world, we have objects that are very expensive to clone. Rust provides us solutions for both of these via other Wrapper types. I’ll try to provide detailed examples of those as well soon.