Associated Types and GATs

On this page

Generic parameters of a trait participate in selecting an implementation, while associated types are determined by the selected implementation. For example, Iterator::Item expresses "what this iterator yields," rather than allowing each next call to arbitrarily select any output type.

Uniqueness is Relative to the Full Set of Implementation Parameters

Iterator has no additional type parameters, so the Item of a concrete iterator type is determined by that implementation. If the trait itself has generic parameters, the implementation must be determined together with those parameters; you cannot generalize this to "with associated types, each Self can only ever have one impl."

trait Convert<Input> {
    type Output;
    fn convert(input: Input) -> Self::Output;
}
struct Decoder;
impl Convert<u8> for Decoder {
    type Output = u16;
    fn convert(input: u8) -> u16 { u16::from(input) }
}
impl Convert<bool> for Decoder {
    type Output = &'static str;
    fn convert(input: bool) -> &'static str { if input { "yes" } else { "no" } }
}
fn main() {
    assert_eq!(<Decoder as Convert<u8>>::convert(7), 7u16);
    assert_eq!(<Decoder as Convert<bool>>::convert(true), "yes");
}

Input is the input used to select the implementation, and Output is the result specified by that implementation; the fully qualified syntax clarifies both. The constraint Iterator<Item = u8> requires that the associated type of the selected implementation equals u8. Associated Types

Equality Constraints and Capability Constraints Answer Different Questions

I: Iterator<Item = u8> requires the output to be exactly u8; I: Iterator, I::Item: Display only requires the output to be formattable. The latter interface can accept multiple output types, and the caller does not need to convert them all to String first.

use std::fmt::Display;
fn joined<I>(input: I) -> String
where
    I: IntoIterator,
    I::Item: Display,
{
    let mut output = String::new();
    for (index, value) in input.into_iter().enumerate() {
        if index != 0 { output.push(','); }
        use std::fmt::Write;
        write!(&mut output, "{value}").unwrap();
    }
    output
}
fn main() {
    assert_eq!(joined([1, 2, 3]), "1,2,3");
    assert_eq!(joined(["a", "b"]), "a,b");
    assert_eq!(joined(std::iter::empty::<u8>()), "");
}

The formatting here is simple concatenation, not CSV serialization: if the input itself contains commas or newlines, the function does not escape them. Associated type constraints describe operations available at compile time; they do not supply data format rules for the algorithm.

SyntaxRelationship Expressed
T: Convert<u8>Selects the implementation of T for u8
<T as Convert<u8>>::OutputProjects the output type specified by that implementation
T: Convert<u8, Output = u16>Further requires the output to equal u16
I::Item: DisplayDoes not fix the output type, only constrains its capabilities

What Can a Standard Iterator Borrow?

std::slice::Iter<'s, T> can implement Item = &'s T, so the statement "Iterators cannot return references" is false. These references are borrowed from the external slice pointed to by the iterator, and the lifetime 's is already part of the iterator type.

The difficulty lies in having the returned item borrow from the current &mut self, rather than a pre-fixed external lifetime. For example, if the iterator internally reuses a read buffer, the next read will overwrite the previous content; or if lending overlapping mutable windows sequentially, you must prevent the caller from holding two windows simultaneously.

Why Standard IterMut Can Hold Two &mut Simultaneously

fn main() {
    let mut values = [1, 2, 3];
    let mut iter = values.iter_mut();
    let first = iter.next().unwrap();
    let second = iter.next().unwrap();
    *first = 10;
    *second = 20;
    assert_eq!(values, [10, 20, 3]);
}

The two returned references can be used simultaneously because IterMut hands out different elements each time and guarantees that subsequent iterations will not hand out the same position that has already been borrowed. Its Item is &mut T on the external slice's lifetime, not a short borrow of the iterator from this next call. IterMut

Overlapping windows cannot make this guarantee: the first window of length 2 covers indices 0 and 1, the second covers 1 and 2. If both were allowed to write simultaneously, index 1 would have two active exclusive access points. Therefore, the key difference is whether the data overlaps and how long the interface allows the return value to live; it is not "returning a mutable reference requires GAT."

GATs: Associated Type Families That Vary with Borrowing

next(&'a mut self)Exclusive borrow of the iteratorItem<'a> = &'a mut [u8]Returns a borrow of the windowGATs bring the lifetime of the current borrow into the return typeCannot borrow the iterator again while the window is still in useCan only obtain the next window after finishing use of the current oneThe Item of a standard Iterator is fixed for an implementation; GATs can form a type family varying with the current borrow.
trait LendingIterator {
    type Item<'a> where Self: 'a;
    fn next(&mut self) -> Option<Self::Item<'_>>;
}

struct Windows<'s> {
    data: &'s mut [u8],
    width: usize,
    position: usize,
}
impl<'s> Windows<'s> {
    fn new(data: &'s mut [u8], width: usize) -> Self {
        assert!(width > 0);
        Self { data, width, position: 0 }
    }
}
impl LendingIterator for Windows<'_> {
    type Item<'a> = &'a mut [u8] where Self: 'a;
    fn next(&mut self) -> Option<Self::Item<'_>> {
        let end = self.position.checked_add(self.width)?;
        if end > self.data.len() { return None; }
        let start = self.position;
        self.position += 1;
        Some(&mut self.data[start..end])
    }
}
fn main() {
    let mut bytes = [1, 2, 3];
    let mut windows = Windows::new(&mut bytes, 2);
    {
        let first = windows.next().unwrap();
        first[1] = 9;
    }
    {
        let second = windows.next().unwrap();
        assert_eq!(second, &[9, 3]);
        second[0] = 8;
    }
    assert!(windows.next().is_none());
    assert_eq!(bytes, [1, 8, 3]);
}

Item<'a> brings the length of the current borrow into the return value. where Self: 'a means that when using this associated type, the borrows involved in Self satisfy that lifetime requirement; it does not require Self to live forever.

If you call windows.next() again while still using first, the compiler will reject the overlapping exclusive borrows. Windows can share underlying positions, but they cannot be accessed simultaneously through two active mutable borrows. This example does not require unsafe.

Index 0Initial value 1Index 1Initial value 2Index 2Initial value 3first: indices 0, 1; writes 1 to 9second: indices 1, 2; reads 9 firstFirst borrowAfter first use endsOverlapping windows share positions but must be lent sequentiallyModifications to index 1 are retained in the underlying array; ending a borrow does not roll back data, nor does it require copying the window.

Consume Item by Item, or Explicitly Take Independent Ownership

If the processing logic only needs to calculate statistics, you can immediately obtain an owned result after each next, and then fetch the next item. If you want to save the complete window, you can clone it into a Vec; this adds allocation and copying, but clearly decouples the result from the borrow of the current window. Collecting window references directly conflicts with the safety goals of overlapping mutable windows.

In the main of the previous example, the braces around first are to clearly demonstrate the order of use; borrows are usually analyzed based on subsequent usage, not necessarily waiting for the lexical scope of the variable to end. If you obtain second first, and then read first, the compiler must consider first still active, and thus reject the second exclusive borrow.

Boundary InputBehavior of Windows in the ExampleReason
width = 0Panic during constructionThe example explicitly excludes empty window semantics; public interfaces could use Result instead
width greater than data lengthFirst next returns NoneNo complete window exists
width equals data lengthProduces only one windowThe next start point cannot accommodate width
Width 2, data length 3Produces two overlapping windowsThe start point increments by 1 each time
Start + width overflowsnext returns Nonechecked_add rejects unrepresentable endpoints

What Does where Self: 'a Constrain?

Expanding the method signature, you can understand it as fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>. Item is no longer a fixed type, but a type family that accepts lifetime parameters; the 'a selected for this call enters the return type, allowing the borrow checker to correlate "still using the window" with "still borrowing the iterator."

For Windows<'s> containing external borrows, Self: 'a requires these borrows to be valid for 'a; you cannot derive longer windows from shorter-lived underlying data. The where clause on the GAT declaration provides preconditions that the implementation can rely on; the example needs to write it because of the borrowing relationship with the method borrowing self. It does not mean "the iterator object must live until the end of the program," nor is it a runtime lifetime check. GATs and Required Where Clauses

This trait is not std::iter::Iterator, so standard iterator combinator methods cannot be automatically applied. GATs expand the expressible interface but do not automatically generate a corresponding adapter ecosystem; you should also consider the current compiler's support boundaries for complex constraints and dyn compatibility.

Generic Usage Does Not Equal Automatic dyn Support

GATs can describe Item<'a>, but lending interfaces with such associated types cannot directly become dyn LendingIterator based on this. The standard approach of dyn Iterator<Item = u8> fixing a single output type does not automatically solve an entire type family that varies with lifetimes. When dynamic boundaries are needed, consider putting processing actions inside trait methods, or returning owned data; the choice depends on whether copying is allowed and what compositional capabilities the caller needs.

Complex consumers may also need for<'a> higher-ranked lifetime constraints. This means satisfying conditions for all quantified lifetimes; you cannot mechanically substitute it with a single named lifetime or 'static. When encountering compiler diagnostics, first narrow down to the actual trait, where clauses, and compiler version verification; do not blindly add 'static to borrowed data just to eliminate errors.

Why cannot overlapping windows be retained together?

First see how width-three windows share elements, then move the second window until disjoint. The lending interface still blocks borrowing self again while the old output remains needed: its signature ties output to that borrow, rather than proving arbitrary window arithmetic at each call. Ordinary IterMut instead guarantees disjoint outputs through its implementation.

Preparing the visual
Why cannot overlapping windows be retained together?

A lending output is tied to this &mut self; retaining it for later use blocks another next even when selected ranges happen not to overlap.

Return-Position impl Trait: Hiding a Concrete Type

fn selected(even: bool) -> impl Iterator<Item = i32> {
    (0..6).filter(move |value| (*value % 2 == 0) == even)
}
fn main() {
    assert_eq!(selected(true).collect::<Vec<_>>(), [0, 2, 4]);
    assert_eq!(selected(false).collect::<Vec<_>>(), [1, 3, 5]);
}

The behavior of the two branches is determined by the bool captured by the closure, and the returned hidden type remains the same. If you return Range on one side of an if and Filter on the other, they will not automatically unify just because they both implement Iterator; you need a unified structure, an explicit enum, or dyn to erase the differences. impl Trait

The hidden type can depend on the function's generic parameters, and lifetime capture rules also vary by edition, so the statement "there is only one global type for all calls" is also an oversimplification. The trait constraints promised by the interface are capabilities the caller can rely on, not the exposure of underlying iterator composition names.

Using the Same Structure to Accommodate Different Runtime Choices

The previous selected puts the choice into the closure state, so both calls have the same return type. Another common case is "possibly no elements": you can turn the branch into an Option, and then return the same IntoIter.

fn maybe_value(enabled: bool) -> impl Iterator<Item = u8> {
    enabled.then_some(7).into_iter()
}
fn main() {
    assert_eq!(maybe_value(true).collect::<Vec<_>>(), [7]);
    assert!(maybe_value(false).collect::<Vec<_>>().is_empty());
}

If you change the true branch to return std::iter::once(7) and the false branch to return std::iter::empty(), although they have the same Item, they are different concrete types and cannot directly become the same return-position impl Iterator. This limitation is unrelated to the number of elements; the same type can completely produce different numbers of elements at runtime.

Connection with Async Traits

GATs can express associated Future types with borrows, making the interface explicitly state the relationship between the return value and &self. However, this does not mean that stable Rust can arbitrarily write impl Future in every associated type definition to name anonymous async blocks.

Native async fn in trait and return-position impl Trait in trait have been stabilized in Rust 1.75 and can be used for corresponding static calls; they also do not automatically make the trait usable as dyn. When dynamic erasure of Future is needed, common interfaces return Pin<Box<dyn Future<Output = T> + 'a>>, and additionally decide whether to require Send. For actual async mechanisms, see async/await, and for object limitations, see trait objects.