Search code examples
rustlifetimeborrowing

Problems with lifetime/borrow on str type


Why does this code compile?

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let x = "eee";
    let &m;
    {
        let y = "tttt";
        m = longest(&x, &y);
    }
    println!("ahahah: {}", m);
}

For me, there should be a compilation error because of lifetimes. If I write the same code with i64, I get an error.

fn ooo<'a>(x: &'a i64, y: &'a i64) -> &'a i64 {
    if x > y {
        x
    } else {
        y
    }
}

fn main() {
    let x = 3;
    let &m;
    {
        let y = 5;
        m = ooo(&x, &y);
    }
    println!("ahahah: {}", m);
}

The error is:

error[E0597]: `y` does not live long enough
   --> src/main.rs:103:25
    |
103 |       m = ooo(&x, &y);
    |                   ^^ borrowed value does not live long enough
104 |   }
    |   - `y` dropped here while still borrowed
105 |   println!("ahahah: {}", m);
    |                          - borrow later used here

Solution

  • There are a few things we need to know to understand this. The first is what the type of a string literal is. Any string literal (like "foo") has the type &'static str. This is a reference to a string slice, but moreover, it's a static reference. This kind of reference lasts for the entire length of the program and can be coerced to any other lifetime as needed.

    This means that in your first piece of code, x and y are already both references and have type &'static str. The reason the call longest(&x, &y) still works (even though &x and &y have type &&'static str) is due to Deref coercion. longest(&x, &y) is really de-sugared as longest(&*x, &*y) to make the types match.

    Let's analyze the lifetimes in the first piece of code.

    fn main() {
        // x: &'static str
        let x = "eee";
        // Using let patterns in a forward declaration doesn't really make sense
        // It's used for things like
        // let (x, y) = fn_that_returns_tuple();
        // or
        // let &x = fn_that_returns_reference();
        // Here, it's the same as just `let m;`.
        let &m;
        {
            // y: &'static str
            let y = "tttt";
            // This is the same as `longest(x, y)` due to autoderef
            // m: &'static str
            m = longest(&x, &y);
        }
        // `m: &static str`, so it's still valid here
        println!("ahahah: {}", m);
    }
    

    (playground)

    With the let &m; you may have meant something like let m: &str to force its type. This I think actually would ensure that the lifetime of the reference in m starts with that forward declaration. But since m has type &'static str anyway, it doesn't matter.


    Now let's look at the second version with i64.

    fn main() {
        // x: i64
        // This is a local variable
        // and will be dropped at the end of `main`.
        let x = 3;
        // Again, this doesn't really make sense.
        let &m;
        // If we change it to `let m: &i64`, the error changes,
        // which I'll discuss below.
        {
            // Call the lifetime roughly corresponding to this block `'block`.
            // y: i64
            // This is a local variable,
            // and will be dropped at the end of the block.
            let y = 5;
            // Since `y` is local, the lifetime of the reference here
            // can't be longer than this block.
            // &y: &'block i64
            // m: &'block i64
            m = ooo(&x, &y);
        } // Now the lifetime `'block` is over.
          // So `m` has a lifetime that's over
          // so we get an error here.
        println!("ahahah: {}", m);
    }
    

    (playground)

    If we change the declaration of m to let m: &i64 (which is what I think you meant), the error changes.

    error[E0597]: `y` does not live long enough
      --> src/main.rs:26:21
       |
    26 |         m = ooo(&x, &y);
       |                     ^^ borrowed value does not live long enough
    27 |     } // Now the lifetime `'block` is over.
       |     - `y` dropped here while still borrowed
    ...
    30 |     println!("ahahah: {}", m);
       |                            - borrow later used here
    

    (playground)

    So now we explicitly want m to last as long as the outer block, but we can't make y last that long, so the error happens at the call to ooo.


    Since both these programs are dealing with literals, we actually can make the second version compile. To do this, we have to take advantage of static promotion. A good summary of what that means can be found at the Rust 1.21 announcement (which was the release the introduced this) or at this question.

    In short, if we directly take a reference to a literal value, that reference may be promoted to a static reference. That is, it's no longer referencing a local variable.

    fn ooo<'a>(x: &'a i64, y: &'a i64) -> &'a i64 {
        if x > y {
            x
        } else {
            y
        }
    }
    
    fn main() {
        // due to promotion
        // x: &'static i64
        let x = &3;
        let m;
        {
            // due to promotion
            // y: &'static i64
            let y = &5;
            // m: &'static i64
            m = ooo(x, y);
        }
        // So `m`'s lifetime is still active
        println!("ahahah: {}", m);
    }
    

    (playground)