rustlifetimeborrow-checkermemory-safety

Clarify the meaning of binding two references to differently scoped referents to the same lifetime in a function signature


I've been trying to get my head around the Rust borrowing and ownership model.

Suppose we have the following code:

fn main() {
    let a = String::from("short");
    {
        let b = String::from("a long long long string");
        println!("{}", min(&a, &b));
    }
}

fn min<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() < b.len() {
        return a;
    } else {
        return b;
    }
}

min() just returns a reference to the shorter of the two referenced strings. main() passes in two string references whose referents are defined in different scopes. I've used String::from() so that the references don't have a static lifetime. The program correctly prints short. Here is the example in the Rust Playground.

If we refer to the Rustonomicon (which I appreciate is a work in progress doc), we are told that the meaning of a function signature like:

fn as_str<'a>(data: &'a u32) -> &'a str

means the function:

takes a reference to a u32 with some lifetime, and promises that it can produce a reference to a str that can live just as long.

Now let's turn to the signature of min() from my example:

fn min<'a>(a: &'a str, b: &'a str) -> &'a str

This is more invloved, since:

Using similar wording to the quoted statement above, what does the function signature of min() mean?

  1. The function accepts two references and promises to produce a reference to a str that can live as long as the referents of a and b? That feels wrong somehow, as if we return the reference to b from min(), then clearly that reference is not valid for the lifetime of a in main().

  2. The function accepts two references and promises to produce a reference to a str that can live as long as the shorter of the two referents of a and b? That could work, since both referents of a and b remain valid in the inner scope of main().

  3. Something else entirely?

To summarise, I don't understand what it means to bind the lifetimes of the two input references of min() to the same lifetime when their referents are defined in different scopes in the caller.


Solution

  • It's (2): the returned reference lives as long as the shorter input lifetime.

    However, from the perspective of the function, both input lifetimes are in fact the same (both being 'a). So given that the variable a from main() clearly lives longer than b, how does this work?

    The trick is that the caller shortens the lifetime of one of the two references to match min()s function signature. If you have a reference &'x T, you can convert it to &'y T iff 'x outlives 'y (also written: 'x: 'y). This makes intuitive sense (we can shorten the lifetime of a reference without bad consequences). The compiler performs this conversion automatically. So imagine that the compiler turns your main() into:

    let a = String::from("short");
    {
        let b = String::from("a long long long string");
    
        // NOTE: this syntax is not valid Rust! 
        let a_ref: &'a_in_main str = &a;
        let b_ref: &'b_in_main str = &b;
        println!("{}", min(&a as &'b_in_main str, &b));
        //                    ^^^^^^^^^^^^^^^^^^
    }
    

    This has to do with something called subtyping and you can read more about this in this excellent answer.

    To summarize: the caller shortens one lifetime to match the function signature such that the function can just assume both references have the same lifetime.