I am testing my understanding of lifetimes in Rust by explicitly annotating function signatures and I created an example that I am not sure I understand.
In this example, I am simulating the concept of sharing a book and turning a page within it. To do this I am using a single mutable reference which I pass to a borrow_and_read
function that updates the curr_page
field of a Book
struct. My Book
struct and main
function look like:
#[derive(Debug)]
pub struct Book<'a> {
pub title: &'a str,
pub curr_page: Option<i32>,
pub page_count: i32,
}
fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
match a_book.curr_page {
Some(page) => a_book.curr_page = Some(page + 1),
None => a_book.curr_page = Some(0),
};
}
fn main() {
let mut the_book: Book = Book {
title: "The Book",
curr_page: None,
page_count: 104,
};
let a_book: &mut Book = &mut the_book;
borrow_and_read(a_book);
borrow_and_read(a_book);
observe_book(&*a_book);
}
pub fn observe_book<'a>(a_book: &'a Book<'a>) {
println!("Observing: {:?}", a_book);
}
(Playground)
For my first implementation of the borrow_and_read
function, I let the compiler add annotations and everything compiled:
fn borrow_and_read(a_book: &mut Book) {
match a_book.curr_page {
Some(page) => a_book.curr_page = Some(page + 1),
None => a_book.curr_page = Some(0),
};
}
I then tried adding a single lifetime annotation specifying a lifetime for both the reference and the instance of the Book
itself:
fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
match a_book.curr_page {
Some(page) => a_book.curr_page = Some(page + 1),
None => a_book.curr_page = Some(0),
};
}
This yielded the following errors:
error[E0499]: cannot borrow `*a_book` as mutable more than once at a time
--> src/main.rs:25:21
|
24 | borrow_and_read(a_book);
| ------ first mutable borrow occurs here
25 | borrow_and_read(a_book);
| ^^^^^^
| |
| second mutable borrow occurs here
| first borrow later used here
error[E0502]: cannot borrow `*a_book` as immutable because it is also borrowed as mutable
--> src/main.rs:27:18
|
24 | borrow_and_read(a_book);
| ------ mutable borrow occurs here
...
27 | observe_book(&*a_book);
| ^^^^^^^^
| |
| immutable borrow occurs here
| mutable borrow later used here
After thinking through what I had initially tried, I decided it made sense to separate the lifetimes of the mutable reference to a Book
and the instance of Book
itself. I then came up with this:
fn borrow_and_read<'a, 'b>(a_book: &'a mut Book<'b>)
where 'b : 'a {
match a_book.curr_page {
Some(page) => a_book.curr_page = Some(page + 1),
None => a_book.curr_page = Some(0),
};
}
which does compile and output the expected results.
I am confused as to why my initial error message was that a_book
was borrowed mutably more than once. I thought I would be ok passing around a single mutable reference since each usage of the reference understood that the reference was mutable. This thinking seems to be confirmed by the final implementation of my borrow_and_read
function but I am not completely sure why specifying that the lifetime of the Book
instance outlives the mutable reference with where 'b : 'a
fixes my issue.
I am hoping to get a solid understanding of how using the same lifetime for both the mutable reference and Book
instance yield the errors I got.
The problem with your original is that the lifetimes are too constrained. By making the borrow on the Book
have the same length as the borrow on the book title ("The Book"
), the mutable borrow is forced to last as long as the actual book itself, meaning that it can never be immutably borrowed.
Let's explore that. It'll be easier to examine your fixed version and then look at what the original does to constrain it.
fn borrow_and_read<'a, 'b>(a_book: &'a mut Book<'b>)
where 'b : 'a {
match a_book.curr_page {
Some(page) => a_book.curr_page = Some(page + 1),
None => a_book.curr_page = Some(0),
};
}
This function has two lifetime parameters: one for the book itself and one for the mutable borrow on the book. We also constrain 'b: 'a
, meaning that any borrows with lifetime 'a
are valid for no longer than borrows with lifetime 'b
. This is actually redundant, since the compiler can see that anyway. By having an argument with type &'a mut Book<'b>
, 'a
already can't last longer than 'b
.
Now let's look at main
. We'll call the lifetime on the book itself 'book
. We'll call the lifetime on the mutable borrow of the book 'mtb
. Finally, we'll call the immutable borrow (at observe_book
) 'imb
. Let's see how long each lifetime has to last.
// Initialize `the_book`. 'book has to start before this.
// Mutably borrow `the_book`. 'mtb has to start here.
let a_book: &mut Book = &mut the_book;
// Use the mutable borrow. 'mtb has to still be valid.
borrow_and_read(a_book);
// Use the mutable borrow. 'mtb has to still be valid.
borrow_and_read(a_book);
// Deref the mutable borrow and reborrow immutably.
// 'imb has to start here, so 'mtb has to end here.
// 'imb is a reference to `the_book`, so 'book has to still be active.
observe_book(&*a_book);
// The variables are no longer needed, so any outstanding lifetimes can end here
// That means 'imb and 'book end here.
So the crux of the issue here is that with this setup, 'mtb
has to end before 'book
. Now let's look at the original version of the function.
fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
match a_book.curr_page {
Some(page) => a_book.curr_page = Some(page + 1),
None => a_book.curr_page = Some(0),
};
}
Now we only have one lifetime parameter, which forces the lifetime of the title and the lifetime of the mutable borrow to be the same. That means that 'mtb
and 'book
have to be the same. But we just showed that 'mtb
has to end before 'book
! So with that contradiction, the compiler gives us an error. I don't know the technical details of why the error is cannot borrow
*a_bookas mutable more than once at a time
, but I imagine that the compiler thinks of "usages" of a variable similarly to how we talk about lifetimes. Since 'book
has to last until the call to observe_book
and 'mtb
is the same as 'book
, it treats the usage of 'book
as a usage of the mutable borrow. Again, I'm not completely sure about that. It might be worth filing an issue to see if the message can be improved.
I did actually lie a little bit above. While Rust doesn't do implicit type coercion, it does do lifetime coercion. Borrows with longer lifetimes can be coerced to borrows with shorter lifetimes. That ultimately doesn't matter too much here, but it's worth knowing about.
The title of the book, a string literal, has the type &'static str
, where 'static
is a special lifetime that lasts for the whole duration of the program. The data is embedded into the binary of the program itself. When we initialize the_book
, it could have the type Book<'static>
, but it could equally be coerced to Book<'book>
for some shorter lifetime 'book
. When we take the mutable borrow we're forced to have 'book: 'mtb
, but we still have no other constraints.
When we call the one-parameter version of borrow_and_read
, 'book
and 'mtb
have to both be coerced down to a shorter, common lifetime. (in this case, since 'book: 'mtb
, 'mtb
would work - and indeed, it's the longest lifetime that would work). With the two-parameter version, no coercion is necessary. 'book
and 'mtb
can be used as is.
Now when we deref a_book
and reborrow it immutably, no mutable borrows can be active. That means that mtb
and the shorter lifetime that both 'book
and 'mtb
were coerced to have to end. But a_book
has lifetime 'book
and we're using it, so 'book
can't end. Hence the error.
With the two-parameter version, 'book
wasn't coerced to a shorter lifetime, so it could continue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With