RFA-647 · Case file with fixtures · Case 619 of 694 · Compiler evidence
A Generic Trait Reference Return Needs an Explicit Lifetime Contract
References hidden inside generic trait arguments need named lifetime relationships. Bind the trait, receiver, and output deliberately, or redesign with a GAT for per-borrow lending.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Lifetime elision created independent choices inside a generic trait identity and method output where one borrowing contract was intended.
- First discriminating check
- Identify what owns the returned data and bind trait, receiver, and output lifetimes explicitly or model per-borrow lending with a GAT.
Lifetime elision works well in ordinary methods, but a reference placed inside a generic trait argument can create another lifetime choice not automatically tied to self. The failing fixture implements DataAccess<&f64> for a container holding &'a f64 and receives E0803.
The trait argument is part of the implementation identity
DataAccess<T> may have implementations for many T. Writing a reference as T introduces a lifetime even if the source omits its name. The method’s elided output reference may meanwhile be tied to its receiver by elision rules. Rust must prove those choices describe the same contract.
The official E0803 explanation makes the lifetime relationship explicit across trait, impl, receiver, and output. The repaired fixture follows that model.
The Reference documents lifetime elision. I rely on it only when it produces the relationship readers would name themselves.
First decide what the output borrows from
The container stores a reference with lifetime 'a. Returning that stored reference can be valid for 'a, even if the borrow of the container is shorter, provided the API and implementation state the relation correctly.
Other accessors return a view into data owned directly by self; then the output should normally last only as long as the receiver borrow. These are distinct lending models.
I draw owners and arrows before changing annotations. Giving every position 'a can overconstrain the receiver, as the official simple repair does for clarity. A more flexible production trait may use an associated type indexed by the borrow lifetime.
GATs can model per-call lending
A generic associated type can express “for each borrow of self, there is an output type containing that same borrow.” This is useful for iterators, parsers, database rows, and guards whose view cannot outlive access.
The Reference covers associated types, including generic associated types. A lending design is more complex than returning a copied number, so I use it where zero-copy or guard-tied access provides real value.
Sometimes the simplest safe API returns an owned or Copy value. For f64, copying removes the lifetime from the trait entirely. For large data, Cow, Arc, or an explicit guard may match ownership and cost better.
Trait design should precede implementation patching
Adding a lifetime parameter to a public trait changes every implementation and use. I examine whether the trait intends one container lifetime, one borrow lifetime per call, or an owned output before committing to syntax.
Generic T may be too unconstrained if all valid outputs borrow from self. An associated type documents that the implementation chooses the output family. Conversely, a type parameter is appropriate when callers select which conversion they request.
Object safety and dynamic dispatch can also be affected by generic associated types and methods. I test required dyn Trait usage rather than assuming the redesigned trait remains erasable.
Async makes lending more demanding
An async method returning or holding a borrow across await creates a future tied to that borrow. The caller cannot mutate or drop the owner until the future releases it. Locks and guards can make this operationally expensive.
I avoid holding synchronous locks across await and consider owned snapshots for long-running work. A lifetime-correct program can still have deadlocks or poor concurrency.
Tests compile short nested scopes, two sequential borrows, attempted overlapping mutation, and any task-spawn boundary. Runtime tests confirm guard release and copied/owned costs where relevant.
My E0803 checklist
- Which reference is hidden inside the trait’s generic argument?
- Does the output borrow stored external data or data owned by self?
- Which receiver or container lifetime should bound that output?
- Is simple explicit trait lifetime binding sufficient?
- Would a GAT express one output type per receiver borrow more accurately?
- Could an owned, Copy, Cow, Arc, or guard result simplify the contract?
- Must the trait remain usable as a trait object?
- Do async and concurrency tests cover how long the borrow or guard remains live?
The core principle is that a reference inside a generic trait identity still needs a source and duration. E0803 exposes the missing relationship. I model that relationship explicitly, or change the output ownership when borrowing is not worth the coupling.