The problem with borrowing in traits
You're building a custom iterator. It holds a Vec<String> and you want next() to return a &str pointing into that vector. You define a trait:
trait MyIter {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
You try to implement it:
struct StringIter {
data: Vec<String>,
index: usize,
}
impl MyIter for StringIter {
// This fails. &str needs a lifetime.
type Item = &str;
// ...
}
The compiler rejects this. &str is a reference, and references require a lifetime. You try adding a lifetime to the struct and the type:
struct StringIter<'a> {
data: &'a [String],
index: usize,
}
impl<'a> MyIter for StringIter<'a> {
// This fails. The trait doesn't know about 'a.
type Item = &'a str;
// ...
}
The compiler screams again. The trait definition has no lifetime parameter 'a, so the implementation can't use it. The associated type Item is a fixed slot; it cannot change based on the struct's lifetime. You are stuck. The trait needs to say "The item borrows from the iterator," but standard associated types can't express that relationship.
Generic Associated Types (GATs) solve this. They let associated types take generic parameters, usually lifetimes. This allows the associated type to vary based on how long the data is borrowed, tying the return type directly to the lifetime of the implementor.
GATs: Associated types that take parameters
An associated type is a placeholder in a trait. type Item tells implementors to pick a concrete type. A GAT adds a parameter list. type Item<'a> tells implementors to pick a type that can depend on a lifetime 'a.
Think of a normal associated type as a vending machine slot that always dispenses the same can. A GAT is a slot that dispenses a can based on the coin you insert. The coin is the lifetime parameter. The machine logic (the impl block) decides exactly what type comes out for which lifetime.
This turns the associated type into a type-level function. Instead of Item being a single type, Item<'a> is a function that maps a lifetime to a type. The compiler uses this mapping to ensure references returned by the trait methods are valid for exactly the right duration.
Minimal example
Here is the smallest working GAT. It defines a trait where the associated type depends on a lifetime, and an implementation that ties that lifetime to the struct.
/// A trait demonstrating a GAT.
/// The associated type Item takes a lifetime parameter 'a.
trait LendingTrait {
// GAT: Item depends on a lifetime 'a.
// The bound `Self: 'a` ensures the struct lives at least as long as the borrow.
type Item<'a> where Self: 'a;
// Returns an item borrowing for some anonymous lifetime.
fn get(&self) -> Self::Item<'_>;
}
struct Data {
value: String,
}
impl LendingTrait for Data {
// The implementation defines the mapping.
// For any lifetime 'a where Data lives long enough,
// Item<'a> is &'a str.
type Item<'a> = &'a str;
fn get(&self) -> Self::Item<'_> {
&self.value
}
}
fn main() {
let data = Data { value: "hello".to_string() };
let s: &str = data.get();
println!("{}", s);
}
The key lines are type Item<'a> where Self: 'a; in the trait and type Item<'a> = &'a str; in the impl. The where Self: 'a clause is mandatory. It proves that the struct holding the data outlives the reference being returned. Without this bound, you could return a reference to a struct that gets dropped immediately, creating a dangling pointer.
How the compiler checks GATs
When you use a GAT, the compiler performs a higher-rank check. It verifies that the implementation holds for any lifetime 'a that satisfies the bounds.
In the example above, type Item<'a> = &'a str; claims that for any lifetime 'a where Data: 'a, the type is &'a str. The compiler checks the get method to ensure it returns a &'a str for the caller-chosen lifetime. The &self.value expression returns a reference tied to self. Since self has lifetime 'a (enforced by Self: 'a), the reference is valid for 'a. The check passes.
If you tried to return a reference to a local variable, the check would fail. The compiler would see that the reference doesn't live for the arbitrary 'a required by the GAT. This is why GATs are safe. They force you to prove that the borrow is tied to the struct's lifetime, not some shorter scope.
Convention aside: The community almost always writes where Self: 'a in GAT definitions. It is the standard pattern for lending traits. Omitting it is a red flag that the trait might allow dangling references.
Realistic example: A lending iterator
The most common use case for GATs is a "lending iterator." Standard iterators return owned values or references with a fixed lifetime. A lending iterator returns references that borrow from the iterator itself. This is useful when the iterator owns a large buffer and you want to avoid copying data.
/// A trait for iterators that yield references tied to their own lifetime.
trait LendingIterator {
/// The type of item yielded.
/// The lifetime 'a represents the borrow duration.
/// `Self: 'a` guarantees the iterator outlives the borrow.
type Item<'a> where Self: 'a;
/// Advances the iterator and returns the next item.
fn next(&mut self) -> Option<Self::Item<'_>>;
}
/// An iterator that yields &str slices from a vector of strings.
struct WordSliceIter<'a> {
words: &'a [String],
index: usize,
}
impl<'a> LendingIterator for WordSliceIter<'a> {
// The item is a &str borrowing from the iterator's data.
// The lifetime 'b is tied to 'a via the struct's lifetime.
type Item<'b> where Self: 'b = &'a str;
fn next(&mut self) -> Option<Self::Item<'_>> {
if self.index < self.words.len() {
let word = &self.words[self.index];
self.index += 1;
Some(word.as_str())
} else {
None
}
}
}
fn main() {
let words = vec!["hello".to_string(), "world".to_string()];
let mut iter = WordSliceIter { words: &words, index: 0 };
while let Some(word) = iter.next() {
println!("{}", word);
}
}
Notice type Item<'b> where Self: 'b = &'a str;. The GAT parameter is 'b, but the implementation uses 'a. This works because Self is WordSliceIter<'a>, which has lifetime 'a. The bound Self: 'b implies 'a: 'b. The compiler infers that the reference can live for any 'b shorter than 'a. This flexibility is what makes GATs powerful. The caller can choose a shorter lifetime if needed, and the trait still works.
Convention aside: Use '_ in the method return type Option<Self::Item<'_>>. This tells the compiler to infer the lifetime from the context. It is the idiomatic way to write GAT methods. Writing Self::Item<'a> explicitly in the method signature is verbose and often unnecessary.
Pitfalls and compiler errors
GATs introduce complexity. The compiler errors can be cryptic if you don't understand the lifetime relationships.
If you forget the Self: 'a bound in the trait definition, the compiler rejects the code with E0310 (the parameter type may not live long enough). It suspects the iterator could be dropped while the reference remains. The fix is always to add where Self: 'a.
trait BadLending {
// Missing `where Self: 'a`.
type Item<'a>;
}
// Error[E0310]: the parameter type `Self` may not live long enough
GATs also break object safety. You cannot create a trait object for a trait with a GAT. The compiler needs to build a vtable for dynamic dispatch, and GATs introduce an infinite number of possible type mappings based on lifetimes. The vtable cannot handle this.
If you try to use dyn LendingIterator, the compiler rejects it with E0038 (the trait cannot be made into an object). GATs are for static dispatch only. Plan your abstractions accordingly. If you need dynamic dispatch, you must box the values or use a different design.
fn process(iter: &dyn LendingIterator) {
// Error[E0038]: the trait `LendingIterator` cannot be made into an object
}
Another pitfall is over-engineering. GATs are heavy. If your associated type doesn't need a lifetime parameter, don't use a GAT. Standard associated types are simpler and support object safety. Use GATs only when the return type genuinely borrows from the trait implementor.
When to use GATs
Use GATs when your trait needs an associated type that borrows from self or depends on a lifetime parameter. Use GATs when you are building a lending iterator, a cache that returns references to internal data, or a parser that yields slices of input. Use standard associated types when the return type owns its data or has a fixed lifetime unrelated to the trait object. Use opaque types with impl Trait in return position when you want to hide the concrete type but don't need the flexibility of a trait-based abstraction.
Treat the Self: 'a bound as a contract. If you can't satisfy it, your design is flawed. GATs break object safety. Plan your abstractions accordingly.