YALQ (or Yet Another Lifetime Question)


#1

Consider the following code:

#![allow(unused)]
use std::marker::PhantomData;

struct S<'a> {
    _d: PhantomData<&'a i32>
}

fn func<'a>(s: &mut S<'a>, val: &'a i32) {
}

fn main() {
    let mut s = S { _d: PhantomData };
    let val = 3;
    func(&mut s, &val);
}

It fails to compile with this error:

error[E0597]: `val` does not live long enough
  --> src/main.rs:14:19
   |
14 |     func(&mut s, &val);
   |                   ^^^ borrowed value does not live long enough
15 | }
   | - `val` dropped here while still borrowed

However if I change the first argument of func from &mut s to &s the problem goes away. What is the explanation for this? Is it that when I use &s lifetime 'a is is taken to be the smallest possible that’ll do the job whereas when I use &mut s it is expanded such that it starts from the first let binding in main?

Playground link


#2

The explanation has to do with variance/subtyping. The gist is that a &mut T makes T invariant. Variance in Rust is purely in terms of lifetimes (e.g. is S<'a> is a subtype of S<'b> if 'a:'b). When something is variant, you can substitute a longer lifetime for a shorter one (also sometimes referred to as “shrinking” the lifetime).

In this case, you cannot shrink down the lifetime because S<'a> becomes invariant as it’s behind a mutable reference. That means its lifetime cannot be shrunk to “intersect” with the lifetime of val. If this were allowed, then you could store val into s and leave s pointing at dangling memory.


#3

To add to what @vitalyd said about variance/subtyping, here’s how this, if allowed, would let you violate soundness in practice:

struct S<'a> {
    d: &'a i32 // not PhantomData
}

fn func<'a>(s: &mut S<'a>, val: &'a i32) {
    // valid because both lifetimes are 'a
    s.d = val;
}

fn main() {
    let mut s = S { d: &42 };
    {
        let val = 3;
        func(&mut s, &val);
    }
    println!("{}", *s.d);
}

#4

Great. That makes sense. Thanks for the explanation guys.