HRTB with Trait objects

Hi, I wanted to see what happens when HRTBs are used in more complex cases such as trait objects.

I evolved the example from the nomicon with a vector F: for<'a> Fn(&'a T).
I'd now like to try and find an implementation that works via trait object but didn't found a way to make it work

struct Closure<T> {
    data: [u8;10],  
    cbs: Vec<Box<dyn for<'b> Fn(&'b T)>>
}

impl Closure<Data<'_>>
{
    fn call(&self) {
        for f in self.cbs.iter() {
            let d = Data(&self.data, 0_u16);
            (f)(&d);
        } 
    }
}

#[derive(Debug)]
pub struct Data<'a>(&'a [u8], u16);

fn main() {
    let c1 = |v:&Data | {
        println!("CLOSURE 1 {:?}", v);
    };
    
    let c2 = |v:&Data | {
        println!("CLOSURE 2 {:?}", v);
    };
    
    let clo = Closure { 
        data: [1_u8; 10], 
        cbs: vec![Box::new(c1), Box::new(c2)] 
    };
    clo.call();
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error: lifetime may not live long enough
  --> src/main.rs:13:13
   |
10 |     fn call(&self) {
   |             -----
   |             |
   |             let's call the lifetime of this reference `'1`
   |             has type `&Closure<Data<'2>>`
...
13 |             (f)(&d);
   |             ^^^^^^^ argument requires that `'1` must outlive `'2`

error: could not compile `playground` (bin "playground") due to 1 previous error

If I introduce the bound '1: '2, It will not be ok with the lifetime of the closure struct with will not leave long enough.
Have I missed something?

The problem isn't with the trait object or the HRTB, but with your type parameter and impl.

impl Closure<Data<'_>>

This says that we're implementing for Closure<Data<'2>>, for all lifetimes '2 (using the name the compiler assigned).

            let d = Data(&self.data, 0_u16);
            (f)(&d);

Then you try to call the function with a reference to a local variable Data<'1>, which is not and can never be a Data<'2>, because local variables always have shorter lifetimes than any lifetime parameter of the fn or impl. The HRTB doesn't help you because it doesn't interact with the lifetime parameter of Data at all; T = Data<'2>, not Data<'a>. To make it do so, you'd need to give up the generic T and mention Data in the Fn signature:

struct Closure {
    data: [u8;10],  
    cbs: Vec<Box<dyn for<'a, 'b> Fn(&'a Data<'b>)>>
}
4 Likes

Something mentioned in @kpreid's explanation that I'd like to especially highlight is that type parameters, such as the T in struct Closure<T>, always represent a single type. Additionally, types that differ by lifetime are distinct types.

That's why T can never represent Data<'x> for any lifetime 'x. It can only represent Data<'y> for one particular lifetime 'y.


Side note: The Fn(..) traits have special higher-ranked sugar syntax, such that these are the same thing:

Vec<Box<dyn for<'b> Fn(&'b T)>>
Vec<Box<dyn Fn(&T)>>

And so are these:

Vec<Box<dyn for<'a, 'b> Fn(&'a Data<'b>)>>
Vec<Box<dyn for<'a> Fn(&'a Data<'_>)>>
Vec<Box<dyn for<'b> Fn(&Data<'b>)>>
Vec<Box<dyn Fn(&Data<'_>)>>

Stated briefly, the elision for the Fn(..) traits matches that for functions and fn pointers.


Another sidenote: Maybe it's just a contrived example, but you can implement Copy for Data and avoid passing a &Data<'_>.

2 Likes

Thank you very much for the detailed explanation
I'd like to have a more detailed view of how lifetimes work in relation to the compiler. Would you have any books exercices or resources to recommend?

Thanks you very much for your detailed answer. It make sense

I've written some things here, but it doesn't cover everything. It doesn't really cover the HRTB type scenarios of this topic for example. I don't know of a good singular resource for that yet.

This blog post covers some of that ground.

1 Like

Thanks you very much