Search code examples
rustlifetime-scoping

Rust Lifetime Example


I've constructed the most basic example I can for lifetimes to understand them. Consider a function that, according to some condition, returns a reference to an element at an index:

fn get_min<T: PartialOrd + Ord>(left: &Vec<T>, right: &Vec<T>, index: usize) -> &T {
    if &left[index] < &right[index] {
        &left[index]
    } else {
        &right[index]
    }
}

Suppose you add the following lifetimes:

fn get_min<'a, 'b: 'a, 'c: 'a, T: PartialOrd + Ord>(left: &'b Vec<T>, right: &'c Vec<T>, index: usize) -> &'a T {
    if &left[index] < &right[index] {
        &left[index]
    } else {
        &right[index]
    }
}

If the returned reference goes out of scope, will it be dropped, even if the vectors continue not to be dropped? In my case, the vectors stay around for the duration of the program, and I don't want the returned reference to.


Solution

  • The compiler will choose a lifetime 'a that is as small as possible. In some situations where the same lifetime is linked to multiple things, it may be longer than you expect, but regardless the compiler does so to reduce conflicts and therefore accept as many valid programs as possible. That principle is definitely in play here since immutable references are covariant with respect to their lifetimes (meaning they can be minimized).

    Another important part of your question is about dropping references. References do not implement Drop; they have no drop logic and are also Copy (which would conflict with Drop). Though since types holding a lifetime can "lock out" mutations or other action on the referent, it is important to know when this "lock" stops being held. Since non-lexical lifetimes were introduced, this is no longer related to the scope of the reference, but rather ends when the reference is last used. So explicitly dropping references (by introducing a scope) is not ever needed.

    Lets consider an example using your function but from a caller's perspective:

    fn main() {
        let mut vec_a = vec![1, 2, 3];
        let mut vec_b = vec![4, 5, 6];
        
        let min = get_min(&vec_a, &vec_b, 0);
        
        println!("the minimum is: {min}");
        
        vec_a.clear();
        vec_b.clear();
    }
    

    The scope of min starts from its declaration until the end of main. However, if that were the lifetime that it borrowed from vec_a or vec_b, then the .clear() calls would conflict and trigger a compiler error. Instead the compiler allows this since you aren't accessing min after the println! statement and thus that is where the lifetime of 'a ends.

    So to answer your question:

    If the returned reference goes out of scope, will it be dropped, even if the vectors continue not to be dropped?

    The returned reference need not be the lifetime of the vectors, and will most likely be much shorter. If references held on forever that would severely limit what you could do.

    In my case, the vectors stay around for the duration of the program, and I don't want the returned reference to.

    Even if vec_a and vec_b were created statically and thus have a 'static lifetime, references derived from them will borrow from them as small a lifetime as the compiler can make them.

    As a side note, the additional lifetimes 'b and 'c are not necessary here. Since left and right are immutable references, and thus are covariant with respect to their lifetime, their lifetimes will already be as small as possible as deduced at the callsite (just enough to support 'a). In this case a single lifetime is sufficient and expresses the same constraints:

    fn get_min<'a, T: PartialOrd + Ord>(left: &'a Vec<T>, right: &'a Vec<T>, index: usize) -> &'a T {
        ...
    }