Skip to content

← Ownership III: Lifetimes, Explicitly step 22 of 22

Medium Primitives

Generics, traits and lifetimes in one signature

Lifetime parameters and type parameters live in the same angle brackets. Before you can read any real library signature you have to be comfortable seeing them together — including seeing when one of them should not be there at all.

pub fn best_by<T, K>(items: &[T], key: K) -> Option<&T> where K: Fn(&T) -> i64;
pub fn describe<T: Display>(items: &[String], tag: T) -> (String, Option<&str>);
pub fn store<'a, 'b: 'a, T>(long: &'b T, out: &mut Vec<&'a T>);
  • best_by returns the element with the largest key, first wins on ties, None for an empty slice.
  • describe returns (format!("{tag}#{n}"), items.first()) where n is the number of items — the summary is owned, the second half is borrowed.
  • store pushes long into out.

Why only one of the three names a lifetime

This is the part worth internalising. Two of these functions are generic and return references, and neither needs an explicit lifetime:

  • best_by(items: &[T], key: K)key: K is a type parameter with no lifetime position in it, so items is the only lifetime-carrying parameter and elision rule 2 gives the return Option<&T> that lifetime.
  • describe(items: &[String], tag: T) — same shape.

Write them with <'a> anyway and clippy::needless_lifetimes fails your submission. The rule is not “generic code needs lifetime annotations”; the rule is exactly what it always was — name a lifetime when elision cannot work out what you meant, and not before.

store is the one that cannot elide, and it is the reason this problem exists.

'b: 'a — the bound that reads backwards

out holds references valid for 'a. long is valid for 'b. Pushing one into the other is only sound if 'b covers at least all of 'a, and rustc will not take that on faith:

error: lifetime may not live long enough
   ... argument requires that `'b` must outlive `'a`

No error code — this is one of the bare diagnostics. The fix is to say it in the signature:

pub fn store<'a, 'b: 'a, T>(long: &'b T, out: &mut Vec<&'a T>)

'b: 'a reads “'b outlives 'a'b lasts at least as long as 'a, so anywhere a &'a is needed a &'b will do. Most people read the colon backwards the first ten times. Draw the containment picture instead: 'b‘s region contains 'a‘s region. Subtyping follows: &'b T is a subtype of &'a T, so it coerces where the shorter one is expected.

The generated driver does exactly this — out is a Vec<&i64> living in a nested block, while values lives for the whole function. 'b is strictly bigger than 'a, which is the ordinary case, and without the bound the ordinary case does not compile.

Two more things you get for free and should recognise:

  • Implied bounds. out: &mut Vec<&'a T> requires T: 'a, and you never have to write it: rustc infers lifetime bounds implied by the parameter types. (rustc’s allow-by-default explicit_outlives_requirements lint exists to tell you to delete such bounds when you do write them.) Note the asymmetry — only lifetime bounds are implied. Trait bounds never are.
  • Ordering. Lifetimes come first in the parameter list. <T, 'a> is a syntax error, not a style preference.

The pins

pub type KeyFn = fn(&i64) -> i64;
const _: for<'a> fn(&'a [i64], KeyFn) -> Option<&'a i64> = best_by;
const _: for<'a> fn(&'a [String], u8) -> (String, Option<&'a str>) = describe;

They instantiate the generics at concrete types and check the borrowing shape survives. (The KeyFn alias is not decoration — for<'a> fn(&'a [i64], fn(&i64) -> i64) -> Option<&'a i64> written out in full trips clippy::type_complexity, which is warn-by-default and therefore fatal here. Factoring a type out into an alias is the standard answer to that lint.)

There is deliberately no pin on store: an outlives relation between two binders is not expressible in a for<'a, 'b> fn(..) type, and a pin without it would reject the correct answer. store is graded by whether its own body compiles.

The tie trap, again

items.iter().max_by_key(&key) returns the last maximum. Three of the tests have ties. Iterator::reduce with a strict > keeps the first, and reads well: “fold the elements pairwise, keeping the better one”.

A footnote for later: K: Fn(&T) -> i64 is quietly higher-ranked — the Fn-trait sugar expands to for<'r> Fn(&'r T) -> i64, so key must work for a reference of any lifetime. You did not have to write that, and most of the time you never will.

Loading visualization…