Hello everyone in the community, I am a beginner who just started learning Rust recently. During my study of Rust, I attempted to create a custom mutable iterator, but I've been facing compilation issues due to lifecycle-related problems. Below is my code:
error: lifetime may not live long enough
--> src/main.rs:51:9
|
43 | impl<'a> Iterator for RefMutCountIter<'a> {
| -- lifetime `'a` defined here
...
46 | fn next(&mut self) -> Option<Self::Item> {
| - let's call the lifetime of this reference `'1`
...
51 | result
| ^^^^^^ method was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`
What confuses me is that when I implemented RefMutCountIter, I already specified the lifetime 'a for RefMutCountIter::count, but the reference returned by self.count.data.get_mut(self.index) has a lifetime of '1. Why is the lifetime of the objects held in Count::data different from the lifetime of Count itself?
"The lifetime of the objects" doesn't exist. Lifetimes belong to borrows, not the values being borrowed.
The other key thing here, which distinguishes a &mut iterator from an & iterator, is that mutable references are exclusive references; you cannot perform an operation that would result in having two references to the same value. Therefore, it is impossible to write a &mut iterator based on a simple index variable, because the borrow checker cannot check that your index is being incremented correctly.
Concretely, what happens is that:
When your next() is called, it is given a &'1 mut RefMutCountIter<'a>.
This means that any time you reborrow a &mut from a field of RefMutCountIter<'a>, the longest lifetime that reference can possibly have is '1.
When you call get_mut(), you are passing a &'1 mut Vec<u32>.
get_mut() returns a reference with the same lifetime it was given, '1.
The place where this differs from & references is that & references straightforwardly implement Copy, so when you have a RefCountIter<'a> that contains an &'a Count, you can just implicitly copy a &'a Count from that, preserving the 'a lifetime. With &mut references, you can never do that.
All this means that it is generally harder to write an iterator over mutable references than an iterator over immutable references. You have to use special techniques and possibly unsafe code. In this case though, you can and should simply return a std::slice::IterMut obtained from self.data.iter_mut() instead of writing a custom iterator.
I have a longer form example of writing an iterator for &mut [T]here. There's no benefit over just returning or having a newtype wrapper around std::slice::IterMut, but you may find the walkthrough illustrative.
I saw that statement some days ago and was a bit confused -- hoping someone would explain that in more detail.
My feeling was, that at least from the official book, the lifetime of a variable is the scope in which it is defined. So why can you say "Lifetimes belong to borrows, not the values being borrowed."? For details, you might consult Validating References with Lifetimes - The Rust Programming Language with statements like
The error message says that the variable x “does not live long enough.” The reason is that x will be out of scope when the inner scope ends on line 7.
The error message of the compiler seems to confirm this terminology with "
^^ borrowed value does not live long enough "
I have currently no other Rust books available. Has terminology changed from what is used in the official book?
It's an unfortunate overlap in terminology. In standard compiler/PL lingo, variables do have lifetimes. But these are not the same as Rust lifetimes -- those '_ things. We'd be better off if the Rust things were called "regions"[1] or "durations" or something else besides "lifetimes". It would take awhile to make that terminology shift; in the meanwhile, it can be less confusing to call variable liveness something else, like it's "scope".
Unfortunately, most learning materials (including the Book) introduce lifetimes ('_ things) by talking about variable scopes, which compounds the confusion IMO.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
But this is inaccurate: The liveness scope / drop scope of variables are not associated with a lifetime. Lexical scopes are not associated with a lifetime either. And lifetimes haven't been directly tied to lexical scopes since NLL (6 years ago).
It's a way to try to teach (Rust) lifetimes, and even a mental model of lifetimes that is sufficient for a number of scenarios. But it's not actually how the borrow checker works and isn't sufficient to explain or understand every scenario. Worse, it actively confuses people who try to reason about borrow check errors by considering when a variable or field is alive.
IMO we'd be better off if the book and other material distinguished Rust lifetimes and liveness scopes. (But I grant that finding a good way to do that without being too confusing is a big challenge.)
At a broad level, the way the borrow checker works is...
Every borrow is associated with a lifetime, and a type of borrow (shared, exclusive)
The compiler calculates the lifetimes
The compiler knows where every place[2] is alive[3] in the control flow graph
If an alive place has a type with a lifetime in it, that lifetime is also alive
Lifetime constraints ('x: 'y) can also imply that a lifetime is alive
The lifetimes are used in turn to calculate when the borrows are alive[4]
Every use of every place is checked against the live borrows to see if there's a conflict
E.g. reading conflicts with exclusive borrows but not shared ones, moving conflicts with all borrows, ...
The connection with scopes is that executing a destructor and going out of scope are uses that get checked against the borrows.
Note that Rust uses the term lifetime in a very particular way. In everyday speech, the word lifetime can be used in two distinct – but similar – ways:
The lifetime of a reference, corresponding to the span of time in which that reference is used.
The lifetime of a value, corresponding to the span of time before that value gets freed (or, put another way, before the destructor for the value runs).
This second span of time, which describes how long a value is valid, is very important. To distinguish the two, we refer to that second span of time as the value’s scope. Naturally, lifetimes and scopes are linked to one another. Specifically, if you make a reference to a value, the lifetime of that reference cannot outlive the scope of that value. Otherwise, your reference would be pointing into freed memory.
Going back to the book example:
fn main() {
let r; // L1 -- Say the type is &'a i32
{
let x = 5; // L4
r = &x; // L5 -- say the RHS is &'b i32. 'b: 'a
} // L6
println!("r: {r}"); // L8
}
r is alive on L8, so 'a is pretty much the function body. Once created, 'b must be alive whenever 'a is ('b: 'a is required for the assignment to work). So 'b is alive from L5 to L8. That means x is borrowed on L6, but being borrowed conflicts with going out of scope. Thus, we get a borrow check error.
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| --- borrow later used here
The OP said:
Which is an example of what I mentioned earlier:
[Conflating liveness scopes and Rust lifetimes] actively confuses people who try to reason about borrow check errors by considering when a variable or field is alive.