Understanding HRTB

I'm trying to understand HRTB & I found this code example:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
where
    for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

fn do_it(data: &(u8, u16)) -> &u8 {
    &data.0
}

fn main() {
    let clo = Closure {
        data: (0, 1),
        func: do_it,
    };
    println!("{}", clo.call());
}

Anyway, When I remove the HRTB things from the code everything works just fine:

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
where
    F: Fn(&(u8, u16)) -> &u8,
{
    fn call(&self) -> &u8 {
        (self.func)(&self.data)
    }
}

fn do_it(data: &(u8, u16)) -> &u8 {
    &data.0
}

fn main() {
    let clo = Closure {
        data: (0, 1),
        func: do_it,
    };
    println!("{}", clo.call());
}

So what's the solution that HRTB provides here?

I found that there's an implicit HRTB.
Is there a situation where explicit HRTB is required?

Yes, you need to use HRTBs when the lifetime elision rules for HRTBs fail (which they don't in your example) and you can't use a generic lifetime instead. Here is an example where you can't elide the lifetime on PrintWithSuffix and also can't pass a generic lifetime 'a, as the reference we pass to print_with_suffix lives only as long as the function lives (and not for any generic lifetime 'a we slap onto fn foo<'a, T>):

trait PrintWithSuffix<'a> {
    fn print_with_suffix(&self, suffix: &'a u8);
}

impl<'a, T: std::fmt::Display> PrintWithSuffix<'a> for T {
    fn print_with_suffix(&self, x: &'a u8) {
        println!("{self}{x}");
    }
}

fn foo<T>(x: T) where for<'a> T: PrintWithSuffix<'a> {
    let y = &0; // <-- unnamable lifetime
    x.print_with_suffix(y);
}

fn main() {
    foo("hello");
}

Playground.

5 Likes

I found out that all the lifetimes here can be inferred except the one in line 13:

trait PrintWithSuffix {
    fn print_with_suffix(&self, suffix: &u8);
}

impl<T: std::fmt::Display> PrintWithSuffix for T {
    fn print_with_suffix(&self, x: &u8) {
        println!("{self}{x}");
    }
}

fn foo<T>(x: T)
where
    for<'a> T: PrintWithSuffix, // Here
{
    let y = &0; // <-- unnamable lifetime
    x.print_with_suffix(y);
}

fn main() {
    foo("hello");
}

Of course, I just added the lifetime to PrintWithSuffix as an example, it is completely unnecessary. Just imagine it needs to be there (I had a hard time to come up with an example, my bad), then you need to use HRTBs if you want to pass a reference to a local variable.

1 Like

Thank you

1 Like

Higher-ranked lifetime elision is a feature of Fn bounds (and Fn trait object types and fn function pointer types). So if you have a trait that is not one of those, and have a lifetime in the parameters that you need to be higher-ranked, that's a situation where you'll need the explicit syntax.

Example.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.