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.

02. Memory Model, Ownership & Lifetimes7 min read

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).

Variance of &mut T over TSafety Critical
Invariant

A mutable reference must be strictly invariant over its referent type to prevent dangling pointer injection.

1. Lifetimes as Subtypes#

Let ′a'a and ′b'b be lifetime regions. We say that ′a'a outlives ′b'b, written as:

′a:′b  ⟺  ′a is a subtype of ′b'a : 'b \iff 'a \text{ is a subtype of } 'b

Intuitively, if a reference is valid for region ′a'a, it is safe to use it anywhere that requires a shorter lifetime ′b'b. 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 F<T>F<T>, variance answers: If T1T_1 is a subtype of T2T_2 (T1:T2T_1 : T_2), what is the relationship between F<T1>F<T_1> and F<T2>F<T_2>?

Architecture Diagram
Rendering diagram...

Complete Variance Table in Rust#

Type ConstructorVariance over 'aVariance over TReason & Invariant
&'a TCovariantCovariantRead-only access: safe to substitute longer lifetimes.
&'a mut TCovariantInvariantRead/write access: writing shorter-lived references would cause dangling pointers!
fn(T) -> ()N/AContravariantFunction arguments: an argument handler expecting less can accept more.
*const TN/ACovariantRead-only raw pointer.
*mut TN/AInvariantRaw pointer capable of in-place mutation.
UnsafeCell<T>N/AInvariantInterior 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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:

1
2
3
4
5
6
7
8
use 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 Markers
When writing custom unsafe data structures, allocators, or FFI bindings, use PhantomData to communicate your intended variance and drop obligations explicitly to the compiler.

5. Concept Verification#

Test your mastery of subtyping and variance mechanics:

Concept check

Why is an immutable reference &'a T covariant over T, while a mutable reference &'a mut T is invariant over T?

Concept check

If type T1 is a subtype of T2 (T1 outlives T2), how does a contravariant constructor like fn(T) behave?