Lifetimes

Variance and Lifetimes

Variance describes how subtyping between lifetimes ('long <: 'short) or types (Sub <: Super) carries over to composite types such as &'a T, &'a mut T, Vec<T>, or fn(T).

The Three Kinds of Variance

Given a type constructor F<T> and a subtyping relationship Sub <: Super (for example, 'static <: 'a, meaning &'static str <: &'a str):

  1. Covariant: F<Sub> <: F<Super>. The subtyping direction is preserved (longer lifetimes can still substitute for shorter lifetimes inside F).
  2. Invariant: Neither F<Sub> <: F<Super> nor F<Super> <: F<Sub>. No lifetime substitution is allowed; lifetimes must match exact equality.
  3. Contravariant: F<Super> <: F<Sub>. The subtyping direction is reversed (occurs in function argument positions fn(T)).

Variance Table for Standard Rust Types

Type Expression Variance over 'a Variance over T Intuition
&'a T Covariant Covariant Read-only access: a longer-lived reference can always stand in for a shorter-lived one
&'a mut T Covariant Invariant You can shorten the outer borrow 'a, but you cannot change lifetimes inside T
Box<T>, Vec<T>, [T; N] — Covariant Owned containers behave like &T with respect to lifetimes inside T
Cell<T>, RefCell<T>, Mutex<T> — Invariant Interior mutability allows writing T through &Cell<T>, requiring exact match
fn(T) -> U — Contravariant in T, Covariant in U A function accepting any &'short str can safely be called with a &'static str

Why &'a mut T Is Invariant Over T

Why can't &mut &'static str be passed where &mut &'short str is expected? If rustc allowed it, the callee could overwrite the caller's &'static str variable with a pointer to a temporary &'short str, leaving the caller with a dangling &'static str after the temporary is dropped!

What would go wrong if `&mut T` were covariant over `T`:
1. `let mut s: &'static str = "forever";`
2. Pass `&mut s` (type `&mut &'static str`) as `&mut &'short str`.
3. Inside the block, write `*slot = &short_lived_string;`
4. `short_lived_string` is dropped at the end of the block.
5. `s` (typed `&'static str`) now points to freed memory! (Unsound)

Because &'a mut T is invariant over T, the compiler rejects this:

// ❌ does not compile
fn overwrite_with_short<'short>(slot: &mut &'short str, short_str: &'short str) {
    *slot = short_str;
}

fn main() {
    let mut static_slot: &'static str = "initial static string";
    {
        let temp = String::from("temporary string");
        // Error: `&mut T` is invariant over `T`, so `&'static str` cannot shrink to `&'short str` here
        overwrite_with_short(&mut static_slot, &temp);
    }
    println!("{static_slot}");
}
error[E0597]: `temp` does not live long enough
  --> src/main.rs:11:48
   |
 9 |         let temp = String::from("temporary string");
   |             ---- binding `temp` declared here
10 |         // Error: `&mut T` is invariant over `T`, so `&'static str` cannot shrink to `&'short str` here
11 |         overwrite_with_short(&mut static_slot, &temp);
   |                              ----------------  ^^^^^ borrowed value does not live long enough
   |                              |
   |                              argument requires that `temp` is borrowed for `'static`
12 |     }
   |     - `temp` dropped here while still borrowed

Covariance of &'a mut T Over 'a (versus T)

Note the difference between the two parameters of &'a mut T:

  • &'a mut T is covariant over 'a: a mutable reference valid for a long scope 'long can be reborrowed for a shorter scope 'short (&'long mut T -> &'short mut T).
  • &'a mut T is invariant over T: if T itself contains a lifetime (e.g., T = &'b str in &'a mut &'b str), 'b cannot be shortened or lengthened.