Basic usage of lifetime specifiers is making me question my sanity

I came across this code from this post on stackoverflow and the accepted answer did not make sense to me. I posted this question on the learnrust subreddit and the answers contradicted what the official book said about lifetimes.

This is the code in the original post:

struct MyStruct<'a>{
    data: &'a str,
}

fn get<'a>(ms: &'a MyStruct<'a>) -> &'a str{
    ms.data
}

fn set<'a>(ms: &'a mut MyStruct<'a>, x: &'a str){
    ms.data = x;
}

fn main(){
    let mut ms = MyStruct{data: "hello"};
    let foo: &str = get(&ms);
    set(&mut ms, "goodbye");
    println!("{foo}");
}

the compiler complains:

Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `ms` as mutable because it is also borrowed as immutable
  --> src/main.rs:16:9
   |
15 |     let foo: &str = get(&ms);
   |                         --- immutable borrow occurs here
16 |     set(&mut ms, "goodbye");
   |         ^^^^^^^ mutable borrow occurs here
17 |     println!("{foo}");
   |                --- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` due to previous error

So here's how I thought things work

  • we make an instance of MyStruct and populate the data field with "hello", a static string.
  • we call the get() function with an immutable reference to the struct we made, the immutable reference was NOT bound to anything like "let borrow = &ms" , it is a throw-away reference that should be dropped right after the call to get() ends.
  • get() signature defines generic lifetime 'a and assigns it to the input references (the reference to the struct, the data inside it, and the output reference)
  • according to the book, the output reference will live as long as the shortest of the input lifetimes. which is &ms in this case.
  • as a result, foo should only live as long as &ms, but apparently it lives longer than that! why is this happening?

reddit answers said that when I assign 'a to all of the references, I'm essentially extending the lifetime of &ms because of "foo" which receives the output reference and lives long which is why it's conflicting with the mutable reference. But I didn't think callsite bindings have anything to do with the signature of the function. I thought the output reference will live as long as the shortest of both input references.

however, changing the signature of get() function to this makes the code work:

fn get<'a, 'b>(ms: &'a MyStruct<'b>) -> &'b str

the answers I got on reddit said that when I do this, I separate the output reference from &ms and it will be dropped right after the call to get() as expected, and since the output is a 'static string then it will live as long as we want. This makes sense. But I don't get why it didn't work before introducing the 'b lifetime.

This is wrong. &ms creates a borrow of ms, and foo lives as long as that borrow, not as long as the reference &ms itself. &ms doesn't exists anymore after the call to get, but the borrow for the compiler continues to exist as long as foo exists. foo is used in the println, so the borrow lasts until there. But &mut ms creates a mutable reference to ms, and thus a conflicting borrow with the one of foo. By adding the lifetime 'b you say that foo borrows from the value inside ms, not from ms itself, and thus it doesn't conflict anymore. Note that this resolves the conflict, but doesn't make the borrow end earlier!

ps: avoid using the same lifetime for different parameters unless you really want to require them to be the same. Doing otherwise can lead you to weird lifetime errors, especially writing stuff like &'a mut MyStruct<'a> is almost never correct.

8 Likes

explanation

fn main(){
    let mut ms = MyStruct{data: "hello"};
    let foo: &str = get(&ms); // get<'1>(&'1 MyStruct<'1>) -> &'1 str
    set(&mut ms, "goodbye");  // set<'2>(&'2 mut MyStruct<'2>, &'2 str)
    // but to have a mutable reference, '1 must end before '2
    println!("{foo}"); // you're extending '1, causing lifetime overlapping, so err
}

explanation

// the lifetime of "hello"   shortens to 'data
// the lifetime of "goodbye" shortens to 'data
// the type of ms is MyStruct<'data>
// let foo: &'data str = get(&'tmp1 MyStruct<'data>);
// set(&'data mut MyStruct<'data>, &'data str);
// and you're using foo the next line meaning 'data must live long to the println! line

// Note:
// since you have &'data mut MyStruct<'data>, it means the only access
// to MyStruct<'data> is moved into the set function, 
// and no access to ms afterwards can be made.

Explanation with meaningful and correct signatures: Rust Playground

struct MyStruct<'data> {
    data: &'data str,
}

fn get<'data_get, 'a>(ms: &'a MyStruct<'data_get>) -> &'data_get str {
    ms.data
}

fn set<'data_set, 'b>(ms: &'b mut MyStruct<'data_set>, x: &'data_set str) {
    ms.data = x;
}

fn main() {
    let ss = String::from("goodbye");
    let s = String::from("hello");
    let mut ms = MyStruct { data: &s }; // &s: &'s str
    let foo: &str = get(&ms); // get<'1, '2>(&'2 MyStruct<'1>) -> &'1 str
    set(&mut ms, &ss); // &ss: &'ss str; set<'3, '4>(&'4 mut MyStruct<'3>, &'3 str)

    println!("{foo} {m}", m = get(&ms));
}

// 'data_get <=> '1, 'a <=> '2, 'data_set <=> '3, 'b <=> '4

// constrains: 's: '1, 'ss: '3,
//             '1: '2, '3: '4, ...

// denote the type of ms is MyStruct<'data>
//        where 's: 'data, 'ss: 'data, <= this is the core: the lifetime of both &s and &ss shortens to a lifetime 'data
//              'data <=> '1 <=> '3, ...
// the region of 'data lies in the println! line

// resolution:
// let mut ms: MyStruct<'data> = MyStruct{data: &'data s};
// let foo: &'data str = get(&'tmp1 ms);
// set(&'tmp2 mut ms, &'data ss);
// foo: &'data str, m => get(&'tmp3 ms) -> &'data str
4 Likes

I never really made the distinction between references and borrows, I thought they were different terms for the same thing. Thank you!

Thanks for the detailed answer, it will take me time to completely grok everything but I think I get the general idea.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.