Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Variance

Variance is the part of the type system you have been using correctly without knowing it existed, and it stays invisible until the day it doesn’t, and then it ruins your week.

Variance answers a question that, in everyday Rust, has an answer so obvious you never ask it: when I have a &'long T and the function wants a &'short T, can I pass it? You know the answer is yes — references that live longer can be used where references that live shorter are needed. That “yes” is a statement about variance. Specifically, it is the statement that &'a T is covariant in 'a. The longer the lifetime, the more places the reference is acceptable.

Now ask the same question about &'a mut T. Can a &'long mut T be passed where a &'short mut T is wanted? The answer is also yes, and people often shrug and assume the rule generalizes. But ask the other question: can a &'a mut Vec<&'long str> be passed where a &'a mut Vec<&'short str> is wanted? The answer is no, and the reason it is no is that &mut T is invariant in T. That distinction — covariant in the reference’s lifetime, invariant in the referent’s type — is the thing that, once you fail to internalize it, will produce the most cryptic error messages in your career.

Let’s get precise.

Definitions, with care

Variance is a property of a generic type constructor with respect to one of its parameters. Given a type constructor F<_> and a subtyping relation A <: B (read: “A is a subtype of B,” i.e., A can be used wherever B is expected), F is one of:

  • Covariant in its parameter if A <: B implies F<A> <: F<B>. Subtyping is preserved.
  • Contravariant in its parameter if A <: B implies F<B> <: F<A>. Subtyping is reversed.
  • Invariant in its parameter if neither implication holds. Subtyping requires exact equality.

In Rust, the only subtyping that exists in normal code is lifetime subtyping: 'long: 'short (read: “'long outlives 'short”) implies 'long <: 'short for the purpose of variance. Note the direction: the longer lifetime is the subtype. This feels backwards on first encounter and remains slightly disorienting forever. The reason is that a reference good for 'long can be used in any context that needs a reference good for 'short — the long-lived reference is more capable, and “more capable” is what makes something a subtype in this kind of system.

So: &'long T is a subtype of &'short T. &'a T is covariant in 'a. Familiar.

&'a mut T and the surprise

Here is the thing that makes variance suddenly matter. &'a mut T is covariant in 'a (same as &'a T) but invariant in T.

Why invariant in T? Because a &mut T lets you write a T. If &'a mut T were covariant in T, you could pass a &mut Vec<&'static str> where a &mut Vec<&'short str> was expected, and the function could then push a &'short str into the vector, and now you have a Vec<&'static str> containing a non-'static reference. Soundness gone.

The classic illustration:

fn assign<T>(input: &mut T, val: T) {
    *input = val;
}

fn main() {
    let mut hello: &'static str = "hello";
    {
        let world = String::from("world");
        assign(&mut hello, &world); // ERROR
    }
    println!("{hello}"); // would be a dangling reference
}

If &mut T were covariant in T, this code would compile and hello would dangle. The compiler refuses:

error[E0597]: `world` does not live long enough
   --> src/main.rs:8:28
    |
7   |         let world = String::from("world");
    |             ----- binding `world` declared here
8   |         assign(&mut hello, &world);
    |                            ^^^^^^ borrowed value does not live long enough
9   |     }
    |     - `world` dropped here while still borrowed

The error doesn’t mention variance. It almost never does. But variance is what’s enforcing the constraint. The compiler refused to coerce &mut &'static str to &mut &'short str, because &mut T is invariant in T, so it then tried to use the actual lifetime of world everywhere, which was too short.

Memorize this: writing through a reference makes the type position invariant. Reading through a reference (and only reading) keeps the type position covariant. Anything that is “in” or “out” must be invariant.

The variance table

Here is the variance of the standard-library types you actually use. Read it once. Refer back to it.

TypeVariance in TVariance in 'a
&'a Tcovariantcovariant
&'a mut Tinvariantcovariant
Box<T>covariant
Vec<T>covariant
*const Tcovariant
*mut Tinvariant
Cell<T>, RefCell<T>, UnsafeCell<T>invariant
fn(T) -> ()contravariant
fn() -> Tcovariant
fn(T) -> Tinvariant
PhantomData<T>covariant
PhantomData<&'a T>covariantcovariant
PhantomData<&'a mut T>invariantcovariant
PhantomData<fn(T)>contravariant
PhantomData<fn() -> T>covariant
PhantomData<fn(T) -> T>invariant

Two patterns to notice.

First, function pointers. fn(T) -> () is contravariant in T, because a function that takes Animal is more general than a function that takes Dog — it can handle any animal, including dogs. So if you have a fn(Animal), you can use it wherever a fn(Dog) is needed (it accepts strictly more things). Subtyping reversed. Contravariance.

Second, Cell<T> and friends. They are invariant for the same reason &mut T is: they let you write through. The only way to safely permit interior mutation is to disallow type coercion entirely.

Where variance shows up

In ordinary Rust code, variance is invisible. Lifetime subtyping happens silently on every function call, and the variance rules let it. You only notice variance when:

  1. You write a generic struct that stores a *mut T or has interior mutability, and the inferred variance is invariant when you wanted covariant. The fix is PhantomData<T> or PhantomData<*const T> to override the variance.

  2. You write a generic struct that stores a function pointer, and the inferred variance is contravariant when you wanted invariant. Same fix, different PhantomData.

  3. You hit an HRTB inference failure on a closure, and the underlying reason is that the closure’s argument position is contravariant in its lifetime, which makes the inference engine give up. We will revisit this in chapter 3.

  4. You write a self-referential type and need to reason about whether moves preserve soundness. We will revisit this in chapter 6.

  5. An async function signature is rejected because the inferred future has a Send bound that depends on a borrow whose variance the compiler cannot bridge. Chapter 5.

That is most of the surface area where variance bites. None of it shows up in the borrow checker’s first thousand error messages you encounter. All of it shows up in the next thousand.

The PhantomData patterns

PhantomData<T> is a zero-sized type whose only job is to tell the compiler that the surrounding struct logically contains a T, even though it doesn’t, for purposes of:

  • Drop check (the struct counts as containing a T for drop ordering).
  • Variance inference (the struct gets the variance of T).
  • Auto-trait inference (the struct gets T’s Send/Sync).

The variance use is what we care about here. The patterns:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

// "I logically own a T."
// Variance: covariant in T. Drop-check: yes.
struct OwnsT<T>(PhantomData<T>);

// "I have a raw pointer to T but logically own it."
// Variance: covariant. Drop-check: yes. Send/Sync: like T.
struct OwnsTViaPtr<T>(*const T, PhantomData<T>);

// "I borrow a T immutably for 'a."
// Variance: covariant in 'a, covariant in T.
struct BorrowsT<'a, T>(PhantomData<&'a T>);

// "I borrow a T mutably for 'a."
// Variance: covariant in 'a, INVARIANT in T.
struct BorrowsMutT<'a, T>(PhantomData<&'a mut T>);

// "I'm a callback that consumes a T."
// Variance: CONTRAVARIANT in T.
struct ConsumesT<T>(PhantomData<fn(T)>);

// "I'm a token tied to T but I never touch one."
// Use this when you want the marker but want to opt OUT of all
// the auto-trait inheritance and drop-check baggage.
struct TokenForT<T>(PhantomData<fn() -> T>);
}

The last one is the interesting trick. PhantomData<fn() -> T> is covariant in T (function return positions are covariant) but does not count as containing a T for drop check, and it is unconditionally Send + Sync regardless of T. This is the right PhantomData to use when you have a marker type that is logically “associated with” T but you don’t actually own or borrow one.

The wrong PhantomData will compile but will produce surprising errors at the call site, often involving auto-trait bounds that look unrelated to the marker. If you find yourself debugging a Send error on a struct that contains a PhantomData<*const T>, the answer is probably to switch to PhantomData<fn() -> T>.

A worked example

Here is a real bug that variance catches.

#![allow(unused)]
fn main() {
struct Cache<'a, T> {
    items: Vec<&'a T>,
}

impl<'a, T> Cache<'a, T> {
    fn add(&mut self, item: &'a T) {
        self.items.push(item);
    }

    fn get(&self, idx: usize) -> Option<&&'a T> {
        self.items.get(idx)
    }
}
}

You then try:

fn main() {
    let value = String::from("hello");
    let mut cache: Cache<String> = Cache { items: vec![] };
    cache.add(&value);

    let value2 = String::from("world");
    cache.add(&value2);
    drop(value2); // can we?
    println!("{:?}", cache.get(0));
}

The compiler says no, because Cache<'a, T>’s Vec<&'a T> field, behind &mut self in add, makes the lifetime invariant — the cache’s 'a got pinned to the intersection of all the lifetimes you put into it. Once you call add with a reference shorter than the cache’s apparent lifetime, the cache’s 'a becomes that shorter lifetime, and any subsequent use of the cache’s contents respects that.

This is variance silently doing its job. The Vec<T> field is covariant in T, so &'long T can be coerced to &'short T for reads. But add takes &mut self, and self’s Cache<'a, T> is invariant in 'a because the field is read-write through &mut self. So the compiler can’t shrink 'a for the duration of the call — it has to unify the call site’s argument lifetime with 'a. Hence the constraint.

You did not type the word “variance” anywhere. Variance was the mechanism.

Sources and further reading

  • The Subtyping and Variance section of the Rustonomicon — the canonical reference, terse but accurate.
  • Niko Matsakis’s blog posts on variance from the early Rust days, particularly the ones that argued for the current variance inference algorithm.
  • Aaron Turon’s series on the type system internals, which explains why Cell<T> is invariant from first principles.

The next chapter, on HRTBs, depends on this one. Make sure the variance table feels familiar before continuing — &mut T invariant in T, fn(T) -> () contravariant in T, Cell<T> invariant in T. If those feel arbitrary now, they will feel arbitrary in chapter 3, and the HRTB chapter is hard enough on its own.