How to return struct with encapsulated borrow of field of self?

In this playground demo I have a borrowing error which I am not sure if it makes sense:

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/lib.rs:22:9
   |
19 |     pub fn foobar(&mut self) -> BarStruct<'_> {
   |                   - let's call the lifetime of this reference `'1`
20 |         let bs = BarStruct::new(&mut self.the_string);
   |                                 -------------------- first mutable borrow occurs here
21 | 
22 |         self.takes_mut_self();
   |         ^^^^ second mutable borrow occurs here
23 | 
24 |         bs
   |         -- returning this value requires that `self.the_string` is borrowed for `'1`

error: aborting due to previous error

Isn't the return value going to have the same lifetime of &mut self?

If takes_mut_self is executed at this point, then two usable mutable references to the_string exist: one in bs, and the other in self of the takes_mut_self body. This is aliasing mutable (unique) borrows, and isn't allowed.

The fact that BarStruct::new() turns the mutable borrow into an immutable one isn't considered; this is simply an implementation detail of BarString::new. Even if it were, aliasing a mutable and immutable borrow isn't allowed.

For a case where this could go wrong, imagine if takes_mut_self were implemented as:

fn takes_mut_self(&mut self) {
    let old_string = self.the_string;
    self.the_string = String::from("other string");
    drop(old_string);
    // (this would be equivalent without the `drop()` line, I've just added
    // it for explicitness)
}

the_string would have been dropped, and now BarStruct's &str would point to uninitialized memory.

I'm not sure I understand the question about the return value having the same lifetime as &mut self, though. If you're still confused after reading this post, could you elaborate?

Ok, great explanation, thanks. Would this be related to https://doc.rust-lang.org/nomicon/lifetime-mismatch.html#limits-of-lifetimes?

I'm not sure I understand the question about the return value having the same lifetime as &mut self , though. If you're still confused after reading this post, could you elaborate?

Was confused because the error talked about:

- let's call the lifetime of this reference `'1`

And so on...

1 Like

Ah, that makes sense! Right, I completely missed that rustc did that.

With the '1 lifetime, I think it's explicitly saying exactly what you did: that your return value has the same lifetime as &mut self. I don't think it's really useful in this case, but in similar cases maybe it'd help more.

It's trying to help show why there are two mutable borrows at the same time. It tells you one of them explicitly: '1 is the lifetime of self, and your return value must also have this lifetime, so thus self is mutable borrowed from line 20 through the end of the function. Then it tells you the second borrow, which happens in the middle of this, and thus is a second mutable borrow.

I would argue it's more just the borrow checker doing its job right. This code would not be valid if it compiled - the rust compiler explicitly relies on function signatures to tell what things do, and a valid implementation of takes_mut_self could cause bs to be corrupted. This is what the borrow checker is supposed to prevent.

It is technically a limitation of the borrow checker that it can't look at takes_mut_self and tell that it's safe or not. So you're not wrong! But since this is one of the common scenarios the borrow checker is designed to prevent, I think looking at regular lifetime documentation is probably more helpful than nomicon edge cases.

For instance, I think this error is pretty identical to the errors shown in chapter 4 of the book. You're borrowing self rather than a random string s, but the situation is otherwise identical to these two snippets:

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

and

    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);

(more detail on these in the linked book chapter)

1 Like

But note that this is considered a design choice of the language rather than a limitation like some of the other situations. One of the core things that the language guarantees is that if you change the body of a function but leave the signature intact, that will never break code that called that function. This guarantee is absolutely central to making libraries with proper backwards compatibility possible.

The basic issue is that takes_mut_self has a signature that allows it to do bad stuff if your code compiled. Whether it actually does this or not is irrelevant in the eyes of the compiler.

This feature also makes compilation much faster than it otherwise would be. Currently the compiler never has to look inside other functions when checking the validity of a function, but changing this would mean that it would essentially have to look at the entire program at once.

6 Likes

Thank you very much to both of you. I don't understand how there are people so awesome and charitable. I am in awe. Every time, any question, even stupid ones, are answered at their fullest.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.