What are the differences when getting an immutable reference from a mutable reference with self-linked lifetimes?
Asked Answered
P

1

2
struct Foo01<'a> {
    val: u32,
    str: &'a String,
}

fn mutate_and_share_01<'a>(foo: &'a mut Foo01<'a>) -> &'a Foo01<'a> {
    foo
}

fn mutate_and_share_02<'a>(foo: &'a mut Foo01<'a>) -> &'a Foo01 {
    foo
}

fn mutate_and_share_03<'a>(foo: &'a mut Foo01) -> &'a Foo01<'a> {
    foo
}

fn main() {
    let mut foo = Foo01 { val: 16, str: &String::from("Hello ") };
    let foo_mut = &mut foo;

    //let loan = mutate_and_share_01(foo_mut);
    //let loan2 = mutate_and_share_01(foo_mut); //error

    //let loan = mutate_and_share_02(foo_mut);
    //let loan2 = mutate_and_share_02(foo_mut); //error

    let loan = mutate_and_share_03(foo_mut);
    let loan2 = mutate_and_share_03(foo_mut); //good
}

What are differences between these mutate_and_share versions?

Photoengrave answered 10/2, 2021 at 8:20 Comment(1)
In general, the &'a SomeType<'a> pattern should be avoided because it usually does not make sense to link the lifetime of a reference to itself like that. And there's a lot of non-obvious problems that can occur (1 2 3) especially when mutable references are involved.Crooked
P
4

In cases 1 and 2, you're saying that the function borrows the structure for as long as the structure borrows its parameter:

foo: &'a mut Foo01<'a>

this says "foo is borrowed from 'a" (&'a mut) and "foo borrows its parameter for 'a" (Foo01<'a>).

Meaning as far as rustc is concerned a call to this function will necessarily borrow the input forever, as the structure necessarily borrows its input for the entirety of its own lifetime, and thus you get locked out: you can't "unborrow" the input by dropping it so the second call can't work ever.

In case 3 you're relating the parameter of the output to the internal borrow which isn't really true but works well enough at least for this case. The reality is that the two lifetimes are unrelated:

fn mutate_and_share<'a, 'b>(foo: &'a mut Foo01<'b>) -> &'a Foo01<'b> {
    foo
}

Also do note that your third case only works because you're never using loan, so it's immediately dropped before the second line executes. If you do this:

    let loan = mutate_and_share_03(foo_mut);
    let loan2 = mutate_and_share_03(foo_mut); //good
    print("{}", loan.val)

then it's not going to compile because the mutable borrows are overlapping.

Oh, and &String is generally useless. There are use cases for &mut String, but any time you see an immutable reference to a String you'd be better off with an &str. Same with &Vec<T>, not useful, should be &[T].

Prow answered 10/2, 2021 at 8:53 Comment(3)
let foo_mut = &mut foo; // once mut borrow mutate_and_share_03(foo_mut); //twice mut borrow why not happen borrow errorsPhotoengrave
Because non-lexical borrows: since you're not actually using the loan variable, the compiler immediately drops it, the borrow ends, and a new one can be created.Prow
And while you could expect the first call to "move" the mutable reference, Rust will actually implicitly reborrow it in those cases (it's as if you'd written &mut *foo_mut), so it actually creates a nested sub-borrow. See this older answer by Sven Marnach for more details.Prow

© 2022 - 2024 — McMap. All rights reserved.