More formal explanation of ownership, borrowing, lifetimes, and mutability?


#1

There’s been a decent amount written, in the two Rust books, the Nomicon, and lots of blog posts, about Rust’s concepts of ownership, borrowing, lifetimes, and mutability. These are all written as talky tutorials, leading the reader through lots of contrived code examples and so on. That’s fine for what it is, but for some reason there seems to be an extreme reluctance to give relatively pithy and precise definitions and explanations of these terms. I’ve been writing Rust for a couple months now, and I still have only a vague, impressionistic idea of how these concepts work in Rust.

Is there some written material on these concepts that’s a little more precise and formal? Or could someone very briefly explain them to me in a way I might find helpful?

I’ll give an example of a simple thing that I cannot explain based on my current understanding.

$ cat trash.rs
#![allow(unused_variables)]
struct S<'a>(&'a mut u64);

fn main() {
    let mut x = 5;
    let mut y = 6;
    let mut s = S(&mut x);
    let mut t = S(s.0);
    s.0 = &mut y;
}
$ rustc trash.rs
error[E0506]: cannot assign to `s.0` because it is borrowed
 --> trash.rs:9:5
  |
8 |     let mut t = S(s.0);
  |                   --- borrow of `s.0` occurs here
9 |     s.0 = &mut y;
  |     ^^^^^^^^^^^^ assignment to borrowed `s.0` occurs here

error: aborting due to previous error

This error message is strange to me because I have previously inferred (not really explicitly been told) that borrowing a thing means taking a reference to it. But I never take a reference to s.0. Again, what does borrowing actually mean?

And if the code above gives that error, why in the world does the following code not give a similar error?

fn main() {
    let mut x = 5;
    let mut y = 6;
    let mut a = &mut x;
    let b = a;
    a = &mut y;
}

Thanks.


#2

The difference between the two examples is down to mutable reborrows vs mutable references being affine - they move on assignment.

In the first case, s.0 is being reborrowed - this means you effectively have let mut t = S(&mut *s.0). This reborrow means that t has (temporarily) borrowed s.0 - s is still valid but it’s not live (ie you cannot do anything with it) until t goes away. This is the same mechanics as when you pass mutable borrows across functions - the mutable borrow is temporarily reborrowed to pass to a function - when that function returns, and assuming it doesn’t extend the borrow, the reborrow ends and the original mutable borrow is live again.

In the 2nd case the mutable borrow is moved - if you try to do anything with a after it’s assigned to b you’ll get a compiler error saying a has moved. But, you can reassign a - that’s fine.

The reason reborrowing is used in the first case and across function calls, as opposed to moving, is because a move would make code much less ergonomic - you wouldn’t be able to pass mutable borrows and use them again once the call returns.

Let me know if this doesn’t help or if I can try to explain further.


#3

Thank you; this is helpful. So it really is accurate to say that s.0 is being (re)borrowed, and not that *s.0 is being reborrowed? If so, a borrow of an object is indeed not the same as taking a reference to it, I guess.

Any suggestions where to read more about this? A quick Google search suggests that Pierce’s Advanced Topics in Types and Programming Languages talks about affine types. Do you know if that’s a good reference?


#4

I’ve been waiting for someone more knowledgeable to respond to this bit, but @vitalyd didn’t mention it, so perhaps I am just completely confused. But I don’t understand why you’d make this assertion; surely t is an instance of S comprised of a reference to s.0?


#5

surely t is an instance of S comprised of a reference to s.0?

I don’t think that’s right unless I completely misunderstand all the terminology here. I mean, isn’t a reference what you get when you write &?

s.0 is of type &'a mut u64. A reference to s.0 would be of type &&'a mut u64. No such type appears in the program. t is of type S<'_> and t.0 is of type &mut u64.


#6

Yeah, it’s a reborrow in borrow checker terms but it’s still a reference to x. As I mentioned upthread:


#7

I realize I failed to reply to your last post @mrb.

So a borrow is a Rust ownership term. In technical terms it’s a reference. A reborrow is an ownership term as well. When something reborrows a mutable borrow, it takes temporary ownership over that reference. The difference between moving the mutable reference and reborrowing is that a move is permanent whereas a reborrow expires and the original borrower regains it - this is all automatic and doesn’t require any syntax. At least that’s my understanding. You can probably look at the raw ptr address in the reborrow case and see whether it’s stil pointing at the same thing.

Hmm, I don’t know of anything. I picked up the Rust aspects by reading various web sources and listening to more experienced Rust devs/users. If you search online for “Rust affine types” you’ll get some good links.


#8

It seems you think the &'a syntax means something other than a reference, but it’s just a reference with an explicit lifetime; see here: https://doc.rust-lang.org/book/second-edition/ch10-03-lifetime-syntax.html


#9

Yes, that I understood.


#10

Older versions of the Rustonomicon had a section on “liveness” and reborrowing that is somewhat relevant:

https://doc.rust-lang.org/1.15.0/nomicon/references.html#liveness