Subtyping, Variance & The Invariance of Mutable Borrows
Master subtyping through lifetime outliving bounds, formal definitions of Covariance, Contravariance, and Invariance, and why &mut T must be invariant.
Rust does not have classical object-oriented class inheritance, but it does have Subtyping. In Rust, subtyping applies almost exclusively to Lifetimes.
Variance is the mathematical property that governs how subtyping between lifetimes translates to subtyping between complex generic containers (such as references, structs, and function pointers).
A mutable reference must be strictly invariant over its referent type to prevent dangling pointer injection.
1. Lifetimes as Subtypes#
Let and be lifetime regions. We say that outlives , written as:
Intuitively, if a reference is valid for region , it is safe to use it anywhere that requires a shorter lifetime . A longer lifetime is more specific and provides stronger guarantees, meaning it can safely substitute for a shorter one.
2. The Three Categories of Variance#
For a generic type constructor , variance answers: If is a subtype of (), what is the relationship between and ?
Complete Variance Table in Rust#
| Type Constructor | Variance over 'a | Variance over T | Reason & Invariant |
|---|---|---|---|
&'a T | Covariant | Covariant | Read-only access: safe to substitute longer lifetimes. |
&'a mut T | Covariant | Invariant | Read/write access: writing shorter-lived references would cause dangling pointers! |
fn(T) -> () | N/A | Contravariant | Function arguments: an argument handler expecting less can accept more. |
*const T | N/A | Covariant | Read-only raw pointer. |
*mut T | N/A | Invariant | Raw pointer capable of in-place mutation. |
UnsafeCell<T> | N/A | Invariant | Interior mutability primitive. |
3. Why &mut T MUST Be Invariant Over T#
Why can you not pass a &mut &'static str to a function expecting &mut &'a str?
Examine what would happen if &mut T were covariant over T:
12345678910111213141516// HYPOTHETICAL: If &mut T were covariant over T fn overwrite_with_temporary<'a>(slot: &mut &'a str) { let temporary_string = String::from("Short-lived"); // If covariance were allowed: // We could write a reference to 'temporary_string' (lifetime 'temp) // into 'slot' which originally pointed to a 'static reference! *slot = &temporary_string; // 'temporary_string' is dropped here! } fn main() { let mut static_ref: &'static str = "I live forever"; // If &mut &'static str could coerce to &mut &'a str: // overwrite_with_temporary(&mut static_ref); // 'static_ref' would now point to deallocated stack memory! }
Because &mut T is invariant over T, rustc strictly forbids substituting lifetime subtyping inside mutable references, closing this security hole completely.
4. Invariance Virality#
Variance is viral: if a struct contains a single invariant field, the entire struct becomes invariant over that type parameter:
12345678use std::marker::PhantomData; use std::cell::Cell; // Struct invariant over T due to Cell<T> struct InvariantContainer<'a, T> { ptr: &'a T, _marker: PhantomData<Cell<T>>, // Forces invariance over T! }
PhantomData to communicate your intended variance and drop obligations explicitly to the compiler.5. Concept Verification#
Test your mastery of subtyping and variance mechanics:
Why is an immutable reference &'a T covariant over T, while a mutable reference &'a mut T is invariant over T?
If type T1 is a subtype of T2 (T1 outlives T2), how does a contravariant constructor like fn(T) behave?