Bisher haben wir Lifetimes immer als konkrete Variablen behandelt: 'a ist eine bestimmte Lifetime, gewählt bei jedem Funktions-Aufruf. Higher-Ranked Trait Bounds (HRTB) sind eine Erweiterung: sie sagen „dieses Bound gilt für alle Lifetimes" — quantifiziert über die Lifetime, nicht über einen einzelnen Wert. Die Syntax ist for<'a>. HRTB tauchen klassisch bei Closures auf, die mit beliebigen Eingabe-Lifetimes arbeiten müssen. Wer das Konzept beherrscht, versteht den letzten verbleibenden Lifetime-Mysterium.

Das Problem ohne HRTB

Schauen wir uns ein Beispiel an, bei dem normale Lifetime-Bounds nicht reichen.

Rust Problem ohne HRTB
// Funktion akzeptiert eine Closure, die einen &str nimmt
fn use_closure<F>(f: F) where F: Fn(&str) -> usize {
    let s1 = String::from("hello");
    let s2 = String::from("longer-string");
    println!("{}", f(&s1));
    println!("{}", f(&s2));
}

fn main() {
    use_closure(|s| s.len());
}

Diese Funktion compiliert! Aber: was ist die Lifetime des &str in Fn(&str)? Sie ist nicht eine bestimmte Lifetime, sondern eine für jeden Aufruf neue. Die Closure muss für jede mögliche Lifetime funktionieren.

Das ist eigentlich HRTB im Hintergrund — der Compiler interpretiert Fn(&str) als for<'a> Fn(&'a str). Es funktioniert ohne explizite Syntax, weil die Elision die HRTB-Form ableitet.

Wann HRTB explizit nötig wird

Bei komplexeren Signaturen scheitert die Elision. Dann musst du HRTB explizit ausschreiben.

Rust HRTB explizit
// Closure mit komplexem Output, der die Input-Lifetime erbt
fn use_extractor<F>(f: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
    let s = String::from("hello world");
    let first = f(&s);
    println!("{first}");
}

fn main() {
    use_extractor(|s| s.split_whitespace().next().unwrap_or(""));
}

for<'a> Fn(&'a str) -> &'a str sagt: für jede mögliche Lifetime 'a, die Closure nimmt eine Ref mit 'a und gibt eine Ref mit derselben 'a zurück.

Das ist nicht das Gleiche wie <'a, F: Fn(&'a str) -> &'a str>. Letzteres würde bedeuten: es gibt eine bestimmte Lifetime 'a, für die das gilt. Mit HRTB sagst du: für jede.

for-Syntax visualisiert

for<'a> Trait<...> ist eine universelle Quantifizierung über 'a.

Rust for-Syntax
// Mathematisch:
// ∀ 'a. F: Fn(&'a str) -> &'a str
//
// In Rust:
// for<'a> Fn(&'a str) -> &'a str
//
// Bedeutung: Für ALLE Lifetimes 'a, die Closure muss vom Typ
// Fn(&'a str) -> &'a str sein.

fn process<F: for<'a> Fn(&'a str) -> &'a str>(f: F) {
    // Die Closure muss mit JEDER beliebigen Lifetime arbeiten können
    let s1 = String::from("short");
    let s2 = String::from("longer");
    let _ = f(&s1);
    let _ = f(&s2);
}

Anders ausgedrückt: die Closure ist lifetime-polymorph. Sie kann mit beliebigen Lifetimes umgehen, ohne dass beim Aufruf eine bestimmte fixiert wird.

Wann braucht man HRTB wirklich?

HRTB wird explizit nötig in diesen Fällen:

Closures mit Lifetime-verbindendem Output

Rust Verbindender Output
// OHNE HRTB: würde nicht funktionieren, weil 'a ist nicht im Scope
// fn apply<'a, F: Fn(&'a str) -> &'a str>(f: F, ...)

// MIT HRTB: 'a wird beim Closure-Aufruf gewählt, nicht beim Funktions-Aufruf
fn apply<F>(f: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
    let s1 = String::from("a");
    let s2 = String::from("b");
    let _r1 = f(&s1);
    let _r2 = f(&s2);
}

Closure als Argument für mehrere unterschiedliche Lifetimes

Wenn die Closure innerhalb der Funktion mit verschiedenen Ref-Lifetimes aufgerufen wird, muss sie HRTB sein.

Trait-Objekte mit Lifetime-Methoden

Rust dyn HRTB
trait Parser {
    fn parse<'a>(&self, input: &'a str) -> &'a str;
}

// Trait-Object mit HRTB
fn use_parser(p: &dyn Parser) {
    let s = String::from("data");
    let _result = p.parse(&s);
}
// p.parse erzeugt implizit HRTB: for<'a> fn(&'a str) -> &'a str

Bei Methoden mit eigenen Lifetime-Parametern in Trait-Objekten ist die HRTB-Form implizit. Du merkst das selten — nur wenn du die Trait-Methoden auf Trait-Objekten generisch behandelst.

HRTB vs. konkrete Lifetime

Der Unterschied wird klar im Vergleich:

Rust Vergleich
// Variante A: konkrete Lifetime
fn variante_a<'a, F>(f: F)
where F: Fn(&'a str) -> &'a str
{
    // Hier muss 'a beim Funktions-Aufruf bestimmt sein.
    // Aber: woher kommt es? Aus den Argumenten? Es gibt keine ref-Argumente.
    // → 'a ist eine FREE Lifetime, der Aufrufer muss sie bestimmen.
    //   In den meisten Fällen unbrauchbar.
    let _ = f;
}

// Variante B: HRTB
fn variante_b<F>(f: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
    // 'a ist quantifiziert über die Closure. Beim Aufruf von f
    // wird 'a passend zur jeweiligen Aufrufstelle gewählt.
    let s = String::from("test");
    let _ = f(&s);          // 'a = scope von s
}

Variante A funktioniert nur, wenn 'a aus dem Kontext bekannt ist. Variante B mit HRTB funktioniert immer — die Closure-Lifetime ist intern frei wählbar.

HRTB in der Stdlib

Du bist HRTB schon begegnet, ohne es zu wissen.

Rust Stdlib-HRTB
// Iterator-Methoden wie .filter() nutzen HRTB implizit
let v = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = v.iter().filter(|&&x| x % 2 == 0).collect();
// .filter expects: F: FnMut(&Self::Item) -> bool
// Self::Item ist &i32 → die Closure muss &&i32 nehmen
// Implizit: for<'a> FnMut(&'a &i32) -> bool

Iterator-Adapter wie filter, map, find haben implizit HRTB-Form für ihre Closure-Argumente. Daher funktionieren sie mit beliebigen Item-Lifetimes.

Praxis: HRTB in echtem Code

Higher-Order-Funktion mit Lifetime-erhaltender Closure

Rust Process-with-Extractor
pub fn process_strings<F>(strings: Vec<String>, extractor: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
    for s in &strings {
        let extracted = extractor(s);
        println!("{} -> {}", s, extracted);
    }
}

fn main() {
    let words = vec![
        String::from("hello world"),
        String::from("rust language"),
        String::from("learn fast"),
    ];

    process_strings(words, |s| s.split_whitespace().next().unwrap_or(""));
}

Klassisches HRTB-Pattern: Higher-Order-Funktion mit Closure, die für beliebige Eingabe-Lifetimes funktionieren muss.

Trait mit Lifetime-Methode

Rust Parser-Trait
pub trait Tokenizer {
    fn tokens<'a>(&self, input: &'a str) -> Vec<&'a str>;
}

struct WhitespaceTokenizer;
impl Tokenizer for WhitespaceTokenizer {
    fn tokens<'a>(&self, input: &'a str) -> Vec<&'a str> {
        input.split_whitespace().collect()
    }
}

// Verwender mit Trait-Objekt → HRTB implizit
pub fn count_tokens(tokenizer: &dyn Tokenizer, samples: &[String]) -> usize {
    samples.iter().map(|s| tokenizer.tokens(s).len()).sum()
}

fn main() {
    let t: &dyn Tokenizer = &WhitespaceTokenizer;
    let samples = vec![String::from("a b c"), String::from("d e")];
    println!("Total: {}", count_tokens(t, &samples));   // 5
}

Trait mit Lifetime-Methode. Beim Trait-Objekt-Gebrauch ist die HRTB-Form implizit.

Closure als gespeichertes Member

Rust Stored-Closure
pub struct StringProcessor {
    transform: Box<dyn for<'a> Fn(&'a str) -> String>,
}

impl StringProcessor {
    pub fn neu<F>(f: F) -> Self
    where F: for<'a> Fn(&'a str) -> String + 'static
    {
        StringProcessor { transform: Box::new(f) }
    }

    pub fn apply(&self, input: &str) -> String {
        (self.transform)(input)
    }
}

fn main() {
    let upper = StringProcessor::neu(|s| s.to_uppercase());
    println!("{}", upper.apply("hello"));
}

Closure als Struct-Member mit HRTB-Bound. Die Closure muss mit beliebigen Eingabe-Lifetimes funktionieren, daher HRTB.

Iterator-Pipeline mit HRTB

Rust Iter-Pipeline
pub fn filter_strings<F>(items: &[String], pred: F) -> Vec<&str>
where F: for<'a> Fn(&'a str) -> bool
{
    items.iter().filter(|s| pred(s)).map(|s| s.as_str()).collect()
}

fn main() {
    let words = vec![
        String::from("apple"), String::from("banana"),
        String::from("cherry"), String::from("date"),
    ];
    let longs = filter_strings(&words, |s| s.len() > 4);
    println!("{longs:?}");        // ["apple", "banana", "cherry"]
}

Filter-Funktion mit HRTB. Die Predicate-Closure wird mit jeder String-Lifetime aufgerufen.

Mehrere HRTBs in einer Signatur

Rust Multi-HRTB
pub fn pipeline<P, T>(input: String, predicate: P, transform: T) -> Option<String>
where
    P: for<'a> Fn(&'a str) -> bool,
    T: for<'a> Fn(&'a str) -> String,
{
    if predicate(&input) {
        Some(transform(&input))
    } else {
        None
    }
}

fn main() {
    let result = pipeline(
        String::from("hello"),
        |s| s.len() > 3,
        |s| s.to_uppercase(),
    );
    println!("{result:?}");    // Some("HELLO")
}

Zwei HRTBs in einer Signatur. Jede Closure ist unabhängig lifetime-polymorph.

Custom-Trait mit HRTB-Bound

Rust Custom HRTB
pub trait StreamProcessor {
    fn handle<'a>(&self, event: &'a str) -> &'a str;
}

pub fn run_stream<P: StreamProcessor>(processor: &P, events: &[String]) {
    for e in events {
        let response = processor.handle(e);
        println!("{e} -> {response}");
    }
}

struct EchoProcessor;
impl StreamProcessor for EchoProcessor {
    fn handle<'a>(&self, event: &'a str) -> &'a str {
        event
    }
}

fn main() {
    let p = EchoProcessor;
    let events = vec![String::from("a"), String::from("b")];
    run_stream(&p, &events);
}

Trait mit Lifetime-Methode. Beim generischen Verwender wird die HRTB-Form implizit angenommen.

Interessantes

HRTB = for<'a>-Syntax für Lifetime-quantifizierte Bounds.

„Für alle Lifetimes 'a, dieses Trait-Bound gilt". Unterscheidet sich fundamental von einer konkreten Lifetime, die aus dem Kontext stammt.

Klassisch bei Closures mit Lifetime-verbindendem Output.

for<'a> Fn(&'a str) -> &'a str — die Closure verbindet Input- und Output-Lifetime. Geht ohne HRTB nicht.

Oft implizit — Elision macht HRTB für dich.

Fn(&str) in einer Generic-Signatur wird vom Compiler als for<'a> Fn(&'a str) interpretiert. Du merkst HRTB selten, weil es automatisch greift.

Explizit nötig bei komplexen Signaturen.

Bei Closure-Bounds, die Output-Lifetimes mit Input-Lifetimes verbinden. Wenn der Compiler die Elision nicht eindeutig auflösen kann.

HRTB ≠ konkrete Lifetime in <'a>.

fn foo<'a, F: Fn(&'a str)> hat eine spezifische Lifetime, die beim Aufruf festgelegt wird. for<'a> quantifiziert über alle Lifetimes — flexibler und meist das Gewünschte.

Trait-Methoden mit Lifetime-Parameter erzeugen implizit HRTB.

Bei dyn Trait-Usage wird die Trait-Methode als HRTB-Form behandelt. Notwendig für Lifetime-Polymorphie über Trait-Objekt.

Iterator-Adapter nutzen HRTB im Hintergrund.

filter, map, find etc. haben implizit HRTB-Bounds. Daher funktionieren sie mit beliebigen Item-Lifetimes — du merkst es selten.

Wenn der Compiler „may not live long enough“ sagt, prüfe HRTB.

Eine der häufigsten Lösungen für komplexe Closure-Bounds: HRTB-Form explizit ausschreiben. Damit wird die Bedeutung „funktioniert für alle Lifetimes" sichtbar.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Lifetimes

Zur Übersicht