Ownership of the fields of a struct when borrowing with mutable references

Ah okay, thanks a lot!

Cool, although this was from Chapter 5. I'm just worried these concepts are foundational to what I'm going to be seeing next, so I might end up not understanding that if I don't understand the concepts in Chapter 4 well. But sure, maybe I'll do one pass over the book and see if the answers reveal itself ¯\_(ツ)_/¯

"The reference you pass inside must be alive for at least as long as the reference I return".

Okay, so doing something like: I pass a reference to get_x, then call a function inside get_x which frees the reference before the return would throw a compiler error if I write a function with that signature?

  1. A function can not return a reference to local variable. The data the variable stores is deallocated at the end of the block. In other words:

    fn a<'l>()-> &'l String {
      // deallocated at `}`
      let my_local = String::from("words");
      // reference would dangle 
      &my_local
    }
    

    There are exceptions of 'static lifetimes but conceptually that is more important.

  2. If a function inner is within a and reference was accepted by compiler on a (outer fn) that means it lives at least the execution of a, so it won't be freed by b.

This is possible only with static mut or equivalent and would be UB by definition.

1 Like

As others said, not a keyword. I could have called it 'lollipop.[1] The point is that the lifetime on the input and on the output are the same.

And the meaning is, "so long as the return value is in use, the borrow you passed in remains active". The lifetime represents that borrow. And the borrow which gets passed in is a borrow of the whole Point.

In terms of the permission concept...[2] well, by the time the Brown book talks about lifetime in function signatures, they're not talking in terms of the permission concept anymore. I'm not sure if they explain it themselves elsewhere or not. But in broad strokes,

I mean this is a non-breaking change as far as the API goes (it can't make downstream code stop compiling)...

    fn get_x<'this>(&'this mut self) -> &'this mut i32 {
-        &mut self.x
+        &mut self.y
    }

...though I'm sure it'd cause some logic errors :wink:.

A logical invariant could be something like: you return type containing &mut self.y which also has methods that rely on self.x not changing so long as it exists.

Ranty aside

The Brown book covers this in brief... but unfortunately it is a bad explanation.

However, Rust can lose track of exactly which places are borrowed. [...]

The problem is that Rust doesn’t look at the implementation of get_first when deciding what get_first(&name) should borrow. Rust only looks at the type signature, which just says “some String in the input gets borrowed”. Rust conservatively decides then that both name.0 and name.1 get borrowed, and eliminates write and own permissions on both.

Remember, the key idea is that the program above is safe. It has no undefined behavior! A future version of Rust may be smart enough to let it compile, but for today, it gets rejected.

It's not "losing track", the function signature is the contract for both the function body and the caller by design. And the signature requires a borrow of the entire tuple. The example will not compile in a future version of Rust.[3] The (current, intentional) meaning of function signature can be relied on for soundness, and is. If Rust gains function APIs that allow field-level borrowing ("view types") or the like, it will be a new kind of API.

Their "key idea" also hints at one of the worst parts of the Brown book: They present a dangerous and factually incorrect model of Rust UB, as if pretending Rust was C++ and reasoning about a subset of behavior was adequate to explain it. It is not. If you ever venture into unsafe Rust, don't use the Brown book as a guide, it will mislead you.

Whoops, that didn't really address your question. I think the key part is that you had to create a reference to the entire Point to call get_x. And that reference's lifetime is kept alive by uses of the return value. So the permissions for the entire struct don't get returned until the return value stops being used.

It's always the case that the whole struct being borrowed (losing permission) means that all of its fields do, too, recursively. Not just when you call a function.

See this conversation. Incidentally, Mutex::get_mut is an example of relying on the Mutex remaining exclusively borrowed for soundness.


  1. The only special lifetime name so far is 'static. ↩︎

  2. which, recall, is just a mental model and not a technical guide ↩︎

  3. Unless we get a Rust 2, but probably not then either. If borrowing traverses function APIs, borrow checking becomes a global analysis and seemingly trivial changes will break code in surprising ways. ↩︎

1 Like

You can think of lifetime annotations as breadcrumbs used to track where the data has been borrowed from. The name of the lifetime is unimportant ('static is the only exception), but the important thing is that &mut self and the returned loan have the same label, meaning they borrow from the same thing (&'something self -> &'something u32). If the labels were different, it would mean that the returned value came from some other argument or pre-existing loan.
In very common/obvious cases where there's only one obvious relationship between references, Rust doesn't require writing the lifetime names explicitly, but it still acts as if they were there.

When you call a method, the . operator will borrow the object you're calling the method on. So p.get_x() makes the compiler see that &mut self is required, so it starts a new loan of &mut p (all of it). Then everything borrowed from &mut p AKA self is tied to the same loan, keeping its restrictions alive.

1 Like

Understood. Thanks for clarifying!

Thanks for the detailed reply.

I understand this. So, returning the reference to p.x means that that the lifetime of p's reference is also till the last line, the print statement, which means we don't have read permissions on p.y as long as the lifetime of x. So, in case we just print x and don't use it anywhere later, then we can print p.y in the next line and that would be valid code (I tried it out and it works!).

Perfect, that summarizes it.

Got it. I can definitely see myself running into the issue the OP of that thread raised. If I may, I would extend the question: Is it possible to have a set-and-return pattern for add_score without the use of lifetimes? This seems like a common pattern across other programming languages and it makes me wonder whether it is even possible to do so in Rust without the concept of lifetimes.

Despite the name, &mut means exclusive access, not mutable access, per se. Unsafe code, etc, is allowed to rely on this.

This line deserves to be framed!

Are you thinking of something like this?

fn add_score(mut self) -> Self {
        self.score += 1;
        self
    }
1 Like

The two ways I can think of off hand are returning a clone or copy like @2e71828 demonstrated (in which case you return a value independent of what is in the struct), or by having some sort of shared ownership...

// I don't actually recommend this, but for illustration
struct Point {
    x: Rc<Cell<i32>>,
    y: Rc<Cell<i32>>,
}

impl Point {
    fn set_x(&self, x: i32) -> Rc<Cell<i32>> {
        self.x.set(x);
        self.x.clone()
    }
}

...in which case the i32 value accessible in the return type is the same as the one in the struct.

But if references (&, &mut) are involved, lifetimes must be involved. Just as you can't have a Vec with no item type, you always have a Vec<()> or Vec<String> or whatever, you can't have a reference without a lifetime.

The closest thing to not having a lifetime on a reference is &'static _.

2 Likes

Okay, got it. I need to study lifetimes to appreciate this behaviour. Thanks @2e71828!

Well… perhaps it's time for your to know the secret behind lifetimes. It's rightfully hidden by Rust tutorials because school teaches the majority of population to hate math with passion. If truth would have been revealed early then people would have just ignored Rust as “too complicated”.

But, well, if you are on the forum then you are probably curious enough to know the truth: lifetimes are not really the part of your program[1] and here is an existential truth: an alternative rust compiler than ignores them[2].

Why do they even exist, then? Well… correctness and memory safety. If you remove the lifetimes and ask someone “would that code ever try to access stale data that's removed from memory” then this question… couldn't be answered in principle. Not as in “it's very hard to answer it”, but as in “no matter what you do… you wouldn't be answer it in some corner cases”.

And that's where both lifetimes and unsafe come from. Lifetimes form a correctness proof that compiler verifies—for the “nice” code that compiler can verify. And unsafe is an escape hatch that says “I couldn't prove to the compiler that my program is always correct, but trust me, I have some means of doing that”.

For me the epiphany that lifetimes are not part of my program that affects the generated code but more of documentation about correctness of my program that compiler verifies made things much to understand.


  1. There are some exceptions related to function pointers and HRTBs. ↩︎

  2. Actually it implements minimal support that's needed for these few rare cases where they actually affect the generated code. ↩︎

Isn't '_ also special in some sense? I know it isn't permitted in certain contexts.

Depends on how you define “lifetime name”. 'static is a name for a particular lifetime, but '_ isn't; it leaves the compiler to make a contextual choice of lifetime.

A fair way to think of it, yes. I would personally think of '_ as a "lifetime name" in the sense that "it goes where the other lifetimes go" and is mentioned separately alongside 'static in the reference. To that extent, I think saying only 'static is special is a little confusing. But I certainly see why you wouldn't want to call it a "lifetime name" when you put it like that.

Point is, it is as much of a "lifetime name" as _ is a type name. They essentially fill the same niche - "there is something (lifetime or type correspondingly), I acknowledge its existence, but the exact value shall be inferred".