The Box
pointer is a unique pointer. It owns an object of type T that
resides on the heap.
A Box:
Stores its internal object on the heap.
Automatically drops (i.e.: deletes) the contained object when it is itself dropped.
Is itself stored on the stack.
Implements Deref and DerefMut. This means the methods of the contained object can be used directly on the
Boxobject, as if it were the contained object itself.Is a very light object. It is passed quickly from one function to another, takes very little space. Thus it can be used to improve both speed and memory performance, when an object is heavy: instead of storing the object on the stack, we store it on the heap.
31.1.1Putting an object in a Box¶
Let us create a structure with many fields:
#[derive(Debug)]
#[allow(unused)]
struct Book {
title: String,
authors: Vec<String>,
isbn: u64,
year: i16,
editor: String,
npages: usize,
price: f32,
pitch: String,
}We add some methods to it:
impl Book {
fn new(title: String, year: i16, npages: usize, price: f32) -> Self {
Self {
title,
authors: Vec::new(),
isbn: 0,
year,
editor: String::new(),
npages,
price,
pitch: String::new(),
}
}
fn with_editor(mut self, editor: String) -> Self {
self.editor = editor;
self
}
fn with_author(mut self, author: String) -> Self {
self.authors.push(author);
self
}
fn with_isbn(mut self, isbn: u64) -> Self {
self.isbn = isbn;
self
}
fn with_pitch(mut self, pitch: String) -> Self {
self.pitch = pitch;
self
}
fn title(&self) -> &str {
&self.title
}
fn price_per_page(&self) -> f32 {
self.price / self.npages as f32
}
}We create an instance of Book:
let book = Book::new("Le Petit Prince".into(), 1999, 104, 7.50)
.with_editor("Gallimard".into())
.with_author("Antoine de Saint-Exupéry".into())
.with_isbn(2070408507)
.with_pitch("J'ai ainsi vécu seul, sans personne avec qui parler véritablement, \
jusqu'à une panne dans le désert du Sahara, il y a six ans. Quelque chose \
...".into())
;
bookBook { title: "Le Petit Prince", authors: ["Antoine de Saint-Exupéry"], isbn: 2070408507, year: 1999, editor: "Gallimard", npages: 104, price: 7.5, pitch: "J'ai ainsi vécu seul, sans personne avec qui parler véritablement, jusqu'à une panne dans le désert du Sahara, il y a six ans. Quelque chose ..." }Now we move the Book instance into a Box struct:
let book = Box::new(book);Couldn't automatically determine type of variable `book`.
Please give it an explicit type.The new book object, though being a Box, behaves like a plain
Book object:
bookWe can call Book methods on it:
book.price_per_page()31.1.2Size of a Box¶
The size of a Box depends on the stored type:
For a sized type like an integer, a float or a structure, the
Box’s size is one word (8 bytes on a 64-bit architecture) for the pointer to the object.For an unsized type like a slice or trait, the
Boxis a fat pointer and its size is two words (16 bytes on a 64-bit architecture): one word for the pointer to the object, and one word for the size of the object.
See Word to know what is a word.
31.1.3Passing a Box¶
Compared to the 120 bytes of our Book structure, the 8 bytes of the
Box object is 15 times lighter. This seems like a huge improvement
if we need to move often our object to functions.
For instance we could write the following function:
fn print_price_per_page(book: Box<Book>) {
println!("Price per page of {}: {}", book.title(), book.price_per_page());
}When calling it, we would pass it our light Box object that would be
moved:
print_price_per_page(book);31.1.4Is Box really useful?¶
However the following function, that also involves a move would not be less performant:
fn print_price_per_page(book: Book) {
println!("Price per page of {}: {}", book.title(), book.price_per_page());
}This is because, in practice, there is no actual move of the
object. The passed object, Book in one case, Box in the other, is
not moved but resides in its place on the stack inside the caller’s
frame. The callee only gets a pointer to the object inside the
caller’s frame.
31.1.5Recursive type¶
The Box is also useful for defining recursive type. As an example
we are going to create a chain List type as an enum. The type
will be generic, and thus will be able to store any type of values.
Let us define it by making a recursive reference to the type itself.
The following definition states that the Val variant contains a
value and the next element, and that the Nil value marks the
end of the list:
enum List<T> {
Val(T, List<T>),
Nil,
}However an enum can only be defined if we can compute its size. The
size of an enum is the size of its biggest variant. The size of
the Val variant being impossible to compute, this enum
definition is invalid, the compiler will reject it.
We must thus give a fixed size to the Val variant. The Box
structure is exactly what we need. It will move the next element
onto the heap, and it has a fixed size whatever the value it points
to. Here is the new definition:
enum List<T> {
Val(T, Box<List<T>>),
Nil,
}Let us import the enum’s variants to ease their use:
use List::{Val, Nil};We implement a print() that we will use to print the content of a
list:
impl<T> List<T>
where T: std::fmt::Debug {
fn print(&self) {
match self {
Val(x, next) => {
print!("({x:?}, ");
next.print();
print!(")");
},
Nil => print!("Nil"),
}
}
}We can now create a list of, for instance, 32-bit integers, and prints it:
let list = Val(4, Box::new(Val(10, Box::new(Val(-13, Box::new(Nil))))));
list.print();
println!();