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):
- Covariant:
F<Sub> <: F<Super>. The subtyping direction is preserved (longer lifetimes can still substitute for shorter lifetimes insideF). - Invariant: Neither
F<Sub> <: F<Super>norF<Super> <: F<Sub>. No lifetime substitution is allowed; lifetimes must match exact equality. - Contravariant:
F<Super> <: F<Sub>. The subtyping direction is reversed (occurs in function argument positionsfn(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 Tis covariant over'a: a mutable reference valid for a long scope'longcan be reborrowed for a shorter scope'short(&'long mut T -> &'short mut T).&'a mut Tis invariant overT: ifTitself contains a lifetime (e.g.,T = &'b strin&'a mut &'b str),'bcannot be shortened or lengthened.