| |

Rust 114 🦀 Variance Mechanics: Covariance, Contravariance, and Invariance

⏱️ Estimated read: 15 min | |

Variance is the rule that determines whether a generic type preserves, reverses, or ignores the subtyping relationship of its arguments. Rust’s only subtyping is lifetime-based: a reference with a longer lifetime can be used where a shorter lifetime is expected. Variance answers the question that follows: if &'long T is a subtype of &'short T, is Wrapper<&'long T> a subtype of Wrapper<&'short T>? The answer depends on what the wrapper does with the reference, and the compiler infers it from the fields.

This chapter covers the variance rules as they are defined in the Rust Reference and implemented in rustc. It begins with the three variance forms—covariant, contravariant, and invariant—and the table of common types that establishes the base cases. It then explains the inference algorithm that composes field variances and the PhantomData shapes that declare variance for types without real fields. The final sections cover the practical consequence: why &mut T is invariant over T, and how to read a variance error as a variance problem rather than a borrow-checker problem.


Key point: Variance is inferred from field types, not from the type’s name. A struct is covariant over T if every field that mentions T is covariant, contravariant if every field is contravariant, and invariant if any field is invariant or if the fields disagree.


Why variance matters

The lifetime shortening problem. A &'static str is a reference that is valid forever. A function that accepts &'a str for some shorter 'a can safely be called with the 'static reference, because the longer lifetime is a subtype of the shorter one. This is covariance in action: the reference type preserves the subtyping direction . The same question applies to any generic type: does Vec<&'static str> coerce to Vec<&'short str>? The answer is yes, because Vec<T> is covariant over T.

The mutation soundness problem. If &mut T were covariant over T, a &mut &'static str could be coerced to &mut &'short str. A function receiving the coerced reference could write a &'short str into the slot that the caller believes holds a &'static str. When the 'short lifetime expires, the caller’s slot contains a dangling reference. Invariance prevents this: &mut T is invariant over T, so the coercion is rejected at compile time .

The composition problem. A user-defined type’s variance is not a single rule; it is the composition of the variances of its fields. A struct with a &'a T field and a *mut U field is covariant over 'a and T, but invariant over U, because the raw mutable pointer is invariant. If a parameter appears in both a covariant and a contravariant position, the result is invariant, because the two directions cancel .

The PhantomData declaration problem. A type with no real field of type T still needs a variance for T. The PhantomData marker supplies it: PhantomData<T> is covariant, PhantomData<fn(T)> is contravariant, and PhantomData<fn(T) -> T> is invariant. The shape of the phantom is the declaration of the intended variance .


a. The three variance forms and the base table

The Rust Reference defines variance in terms of subtyping. F<T> is covariant over T if T: U implies F<T>: F<U>. It is contravariant if T: U implies F<U>: F<T>. It is invariant otherwise, meaning no subtyping relation is derived .

The base cases are the types that the compiler has built-in variance for. The table is the reference for every composition that follows.

TypeVariance over 'aVariance over T
&'a TCovariantCovariant
&'a mut TCovariantInvariant
*const T—Covariant
*mut T—Invariant
[T] and [T; n]—Covariant
fn() -> T—Covariant
fn(T) -> ()—Contravariant
UnsafeCell<T>—Invariant
PhantomData<T>—Covariant
dyn Trait<T> + 'aCovariantInvariant

The &'a T row is the one that makes lifetime shortening work: a longer reference is a subtype of a shorter one, and the shared reference preserves that direction. The &'a mut T row is the one that prevents the lifetime-laundering bug: the lifetime is covariant, but the target type is invariant, because mutation could otherwise smuggle a short-lived value into a long-lived slot . The UnsafeCell<T> row is the reason Cell<T> and RefCell<T> are invariant: interior mutability allows writing through a shared reference, and the same soundness argument that applies to &mut T applies to them .


b. Inference and composition

For user-defined structs, enums, and unions, the compiler iterates over the fields and accumulates constraints. The rustc-dev-guide describes the algorithm: for each use of a type parameter X, the variance of the use site constrains the variance of X. The constraints are resolved on a lattice where bivariant is the top, invariant is the bottom, and covariant and contravariant are in between. The optimal solution is the most permissive variance that satisfies all constraints .

The composition uses the xform operation from the variance paper. If a field appears at variance V1 within the struct, and the type parameter appears at variance V2 within that field, the resulting variance of the parameter is V1.xform(V2). Covariant composed with covariant is covariant. Covariant composed with contravariant is contravariant. Invariant composed with anything is invariant . A parameter that appears in both covariant and contravariant positions produces a constraint of + and -, which the lattice resolves to invariant, because invariant is the only variance that satisfies both.

The #[rustc_variance] attribute is the compiler’s debugging tool for this. It is an internal attribute that requires #![feature(rustc_attrs)], and it makes the compiler print the inferred variance of every type parameter on the annotated item. It was formerly the trigger for error E0208, but it is now a standalone attribute with no error code. It is the practical way to verify that a type’s variance is what the author intended.


c. PhantomData and the variance declaration

PhantomData<T> is a zero-sized marker that simulates a field of type T for the purposes of variance, drop checking, and auto-trait derivation. The Nomicon’s table is the reference for the shapes and their effects.

Phantom typeVariance over 'aVariance over T
PhantomData<T>—Covariant (with drop check)
PhantomData<&'a T>CovariantCovariant
PhantomData<&'a mut T>CovariantInvariant
PhantomData<*const T>—Covariant
PhantomData<*mut T>—Invariant
PhantomData<fn(T)>—Contravariant
PhantomData<fn() -> T>—Covariant
PhantomData<fn(T) -> T>—Invariant

The choice between PhantomData<T> and PhantomData<fn(T)> is the choice between a type that owns or produces T and a type that only consumes T. An iterator that yields T is covariant, because it only produces values; a callback that accepts T is contravariant, because it only consumes them. A type that both produces and consumes T is invariant, because the two directions cancel .

The PhantomData<&'a T> shape is the one that bounds a lifetime that would otherwise be unbounded. The Nomicon’s Iter<'a, T> example is the canonical case: the struct holds *const T pointers, but it logically borrows &'a T values. Without the phantom, the 'a parameter is unused, and the struct does not compile. With PhantomData<&'a T>, the lifetime is bounded and the struct is covariant over both 'a and T .


Complete Example Session

// ============================================
// PART 1: THE BASE CASES
// ============================================
// Shared references are covariant; mutable are invariant in T.

fn shorten<'long: 'short, 'short>(x: &'long str) -> &'short str {
    x  // covariant: longer lifetime coerces to shorter
}

// fn widen<'short, 'long: 'short>(x: &'short str) -> &'long str {
//     x  // ERROR: cannot extend a lifetime
// }

// ============================================
// PART 2: THE INVARIANT MUTABLE REFERENCE
// ============================================
// &mut T is invariant over T.

fn bad<'short>(r: &mut &'short str, s: &'short str) {
    *r = s;  // if this were covariant, lifetime laundering
}

// The caller cannot pass a &mut &'static str where
// &mut &'short str is expected, because the write
// could replace the static reference with a short one.

// ============================================
// PART 3: THE STRUCT COMPOSITION
// ============================================
// Variance is inferred from fields.

struct Covariant<'a, T> {
    value: &'a T,  // covariant in 'a and T
}

struct Invariant<'a, T> {
    value: &'a mut T,  // covariant in 'a, invariant in T
}

// ============================================
// PART 4: THE PHANTOM DECLARATION
// ============================================
// PhantomData shapes declare variance for unused parameters.

use std::marker::PhantomData;

// Covariant: simulates a shared reference.
struct Producer<T> {
    func: fn() -> T,
    _marker: PhantomData<T>,
}

// Contravariant: simulates a function argument.
struct Consumer<T> {
    func: fn(T),
    _marker: PhantomData<fn(T)>,
}

// Invariant: simulates both directions.
struct InvariantPhantom<T> {
    _marker: PhantomData<fn(T) -> T>,
}

// ============================================
// PART 5: THE LIFETIME BOUNDING
// ============================================
// PhantomData<&'a T> bounds an otherwise unused lifetime.

struct Iter<'a, T> {
    ptr: *const T,
    end: *const T,
    _marker: PhantomData<&'a T>,  // covariant in 'a and T
}

// The 'a parameter is used by the phantom, so the struct compiles.
// The struct is covariant over 'a and T, matching &'a T.

// ============================================
// PART 6: THE CONTRAVARIANT COMPOSITION
// ============================================
// A function argument reverses the subtyping direction.

fn accepts_animal(_: &dyn Animal) {}
fn accepts_dog(_: &dyn Dog) {}

// fn(&dyn Dog) is a subtype of fn(&dyn Animal)
// because a function that accepts any animal can
// stand in for one that only needs a dog.

// ============================================
// PART 7: THE RUSTC_VARIANCE ATTRIBUTE
// ============================================
// The compiler's debugging tool for inferred variance.

// #![feature(rustc_attrs)]
// #[rustc_variance]
// struct Foo<'a, T> { x: &'a T }
// The compiler prints the variance of 'a and T.

// ============================================
// PART 8: THE CELL INVARIANCE
// ============================================
// Interior mutability makes a type invariant.

use std::cell::Cell;

struct CellHolder<'a> {
    value: Cell<&'a i32>,  // invariant in 'a
}

// Cell<&'a i32> is invariant because it allows
// writing through a shared reference.

// ============================================
// PART 9: THE PRACTICAL ERROR
// ============================================
// A variance error looks like a borrow error.

// fn foo<'short>(x: &mut &'static str) {
//     let y: &mut &'short str = x;  // ERROR: invariant
// }

// The error is not about the borrow; it is about
// the variance of &mut T over T.

// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// Variance is inferred from fields.
// Covariant: shared refs, Box, Vec.
// Invariant: &mut T, UnsafeCell, Cell.
// Contravariant: fn(T) argument position.
// PhantomData declares the intended variance.

fn main() {
    let s: &'static str = "hello";
    let shorter: &str = shorten(s);
    assert_eq!(shorter, "hello");
}

The ten parts build the variance rules from the base cases to the composition and the PhantomData declarations. The pattern is consistent: the base types establish the direction, the fields compose it, and the phantom declares it when no field exists.


Quick Reference

Variance Forms

FormSubtyping DirectionMeaning
CovariantT: U → F<T>: F<U>Preserves
ContravariantT: U → F<U>: F<T>Reverses
InvariantNo relationIgnores

Base Cases

TypeOver 'aOver T
&'a TCovariantCovariant
&'a mut TCovariantInvariant
*const T—Covariant
*mut T—Invariant
UnsafeCell<T>—Invariant
fn() -> T—Covariant
fn(T) -> ()—Contravariant

PhantomData Shapes

PhantomOver 'aOver T
PhantomData<T>—Covariant
PhantomData<&'a T>CovariantCovariant
PhantomData<&'a mut T>CovariantInvariant
PhantomData<fn(T)>—Contravariant
PhantomData<fn(T) -> T>—Invariant

Composition Rules

Outer VarianceInner VarianceResult
CovariantCovariantCovariant
CovariantContravariantContravariant
InvariantAnythingInvariant
ContravariantCovariantContravariant
ContravariantContravariantCovariant

Best Practices

✅ Do This:

// Use PhantomData<&'a T> to bound a lifetime.
_marker: PhantomData<&'a T>                                          // ✅
// Use PhantomData<fn(T)> for callbacks that only consume T.
_marker: PhantomData<fn(T)>                                          // ✅
// Review variance when a type stores raw pointers.
// The raw pointer's variance may not be what the API intends.        // ✅

❌ Don’t Do This:

// Don't assume &mut T is covariant over T.
&mut &'long str → &mut &'short str  // ERROR: invariant              // ❌
// Don't use PhantomData<T> without considering drop check.
// PhantomData<T> owns T; use *const T for non-owning.               // ❌
// Don't treat a variance error as a borrow-checker error.
// The fix is in the type's variance, not the borrow.                // ❌

Common Pitfalls

PitfallWhy It HappensFix
Lifetime shortening rejectedType is invariant over the lifetimeUse PhantomData<&'a T> or a covariant field
Cannot coerce &mut &'long&mut T is invariant over TUse & instead of &mut
Cell<T> rejects coercionInterior mutability is invariantAccept the restriction; it is soundness
PhantomData<T> owns TDrop check treats it as ownedUse PhantomData<*const T> for non-owning
Variance disagreementParameter appears in both + and - positionsThe type is invariant; review the design

Real-World Examples

1. Covariant Reference

&'long T → &'short T

2. Invariant Mutable Reference

&'long mut T ↛ &'short mut T

3. Covariant Phantom

PhantomData<T>

4. Contravariant Phantom

PhantomData<fn(T)>

5. Invariant Phantom

PhantomData<fn(T) -> T>

6. Cell Invariance

Cell<&'a T> // invariant in 'a

7. Function Contravariance

fn(&'short T) is a subtype of fn(&'long T)

8. Bounded Lifetime

_marker: PhantomData<&'a T>

9. rustc_variance

#[rustc_variance] // prints inferred variance

10. Composition

struct S<'a, T> { x: &'a T } // covariant in both

Visual

The Subtyping Direction

┌─────────────────────────────────────────────────────────┐
│  COVARIANT                                              │
│  'long : 'short                                         │
│  &'long T  <:  &'short T                                │
│         │                                               │
│         ▼  longer lifetime can be used as shorter       │
│                                                         │
│  CONTRAVARIANT                                          │
│  'long : 'short                                         │
│  fn(&'short T)  <:  fn(&'long T)                        │
│         │                                               │
│         ▼  function accepting shorter is more flexible  │
│                                                         │
│  INVARIANT                                              │
│  No coercion in either direction                        │
└─────────────────────────────────────────────────────────┘

The &mut T Invariance

┌─────────────────────────────────────────────────────────┐
│  IF &mut T WERE COVARIANT:                              │
│                                                         │
│  let mut x: &'static str = "static";                    │
│  let r: &mut &'short str = &mut x;  // hypothetical     │
│  *r = short_ref;  // writes short into static slot      │
│  // short_ref expires → x dangles                       │
│                                                         │
│  INVARIANCE PREVENTS THIS.                              │
└─────────────────────────────────────────────────────────┘

The Phantom Table

┌─────────────────────────────────────────────────────────┐
│  PhantomData<T>              → covariant in T           │
│  PhantomData<&'a T>          → covariant in 'a, T       │
│  PhantomData<&'a mut T>      → covariant 'a, invariant T│
│  PhantomData<fn(T)>          → contravariant in T       │
│  PhantomData<fn(T) -> T>     → invariant in T           │
└─────────────────────────────────────────────────────────┘

Composition

┌─────────────────────────────────────────────────────────┐
│  struct S<'a, T> {                                      │
│      x: &'a T,       // covariant in 'a, T              │
│      y: *mut T,      // invariant in T                  │
│  }                                                      │
│                                                         │
│  'a: only appears in x → covariant                      │
│  T: appears in x (+) and y (o) → invariant              │
│                                                         │
│  The most restrictive constraint wins.                  │
└─────────────────────────────────────────────────────────┘

Summary

ItemValue
CovariantPreserves subtyping (&T, Box<T>, Vec<T>)
ContravariantReverses subtyping (fn(T))
InvariantIgnores subtyping (&mut T, UnsafeCell<T>)
InferenceFrom field types, composed via xform
PhantomData<T>Covariant, with drop check
PhantomData<fn(T)>Contravariant
PhantomData<fn(T) -> T>Invariant
Debugging#[rustc_variance] (nightly)

Key takeaways:

  • Variance is inferred from fields, not declared. A struct’s variance over T is the composition of the variances of every field that mentions T. The compiler does not consider the type’s name or its methods .
  • &mut T is invariant over T for soundness. If it were covariant, a short-lived reference could be written into a slot that promised a long-lived one, and the slot would dangle when the short lifetime expired .
  • UnsafeCell<T> is invariant for the same reason. Interior mutability allows writing through a shared reference, and the write could smuggle a short-lived value into a long-lived slot .
  • PhantomData declares the intended variance. The shape of the phantom—T, &'a T, fn(T), fn(T) -> T—determines the variance of the parameters that have no real field. The Nomicon’s table is the reference .
  • A variance error is not a borrow error. When a coercion is rejected because the type is invariant, the fix is in the type’s variance (change the field, change the phantom), not in the lifetime annotations .
  • Composition is the lattice. Covariant composed with contravariant is contravariant. Invariant composed with anything is invariant. A parameter that appears in both directions is invariant .

Remember: Variance is the rule that decides whether a generic type is allowed to change its argument’s lifetime. It is inferred from the fields because the fields determine what the type can do with the argument: read it, write it, or both. The shared reference reads, so it is covariant. The mutable reference writes, so it is invariant. The function argument only consumes, so it is contravariant. The PhantomData is the declaration for the cases where no field exists. When the compiler rejects a coercion, the error is the variance rule doing its job, and the fix is to change the field or the phantom, not to fight the borrow checker.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!