Hello!
Sorry in advance if my question has been answered elsewhere - I tried to look around for it, but only dug up questions regarding mutable references. Apologies as well if it turns out I was just over-thinking something very basic...
I was recently working on a side-project that involved creating a tree-like data structure, and somewhere along the way, I ended up writing something I was sure the borrow-checker would have a problem with. But to my surprise, it seemed to run and compile just fine! Here's a simplified version of what I was doing:
#[derive(Debug)]
struct SomeStuff {
value: i32,
next: Option<Box<SomeStuff>>,
}
impl SomeStuff {
fn new(value: i32) -> Box<Self> {
Box::new(SomeStuff { value, next: None })
}
fn with(value: i32, next: Box<SomeStuff>) -> Box<Self> {
Box::new(SomeStuff {
value,
next: Some(next),
})
}
}
fn handle(values: &[i32]) -> Box<SomeStuff> {
let mut head = SomeStuff::new(*values.last().unwrap());
let len = values.len();
for &x in values[0..(len-1)].iter().rev() {
let next = SomeStuff::with(x, head);
head = next;
}
head
}
I was a bit surprised that this worked, sinceI thought moving out of head
would invalidate the variable. I tried to add a little print debugging:
let mut head = SomeStuff::new(*values.last().unwrap());
let len = values.len();
for &x in values[0..(len-1)].iter().rev() {
let next = SomeStuff::with(x, head);
println!("next={next:?}, head={head:?}");
head = next;
}
Unsurprisingly, this didn't work at all, since I was now trying to observe head in an invalid state.
(In the end, I removed the print and simplified the loop body down to head = SomeStuff::with(x, head)
.)
Intuitively, I understand why this should be allowed. It's as if I'd written this without a loop:
let head = Box::new(tail());
let head = Box::new(prev(), head);
let head = Box::new(prev(), head);
...
and trying to do this inside the loop would just result in me shadowing the outer variable.
So my questions are
- Is there a documented reason this is allowed?
- Has this always been allowed since 1.0, or was there something like an improvement to the borrow checker that allowed this?
Looking back at the Rust Book and Rust Reference though, I can't find anything that gives me a clear picture of how this would be modeled. My best guess is that while the original binding for head
was invalidated by moving it, mut
might allow me to create new bindings - as if I were able to shadow it with a new name.
So it might be that simple, but it does feel like a slightly... "dynamic"? thing for older versions of the borrow checker to have picked up on - seeing that head
is invalidated when I move out of it in the loop, but noticing that re-bind it before the end of the loop, making it valid again by the start of the next loop iteration. I guess it can't be that bad if it works currently though