fbilhaut
fbilhaut

Reputation: 76

How does boxing a trait affect lifetime of parameters passed to it ? (with a very specific example)

Here is a very simple but specific example that produces a compilation error I cannot understand:

use std::path::Path;

trait MyTrait<T> {
    fn my_func(&self, t: T);
}

struct MyImpl {}

impl MyTrait<&Path> for MyImpl {
    fn my_func(&self, t: &Path) {
        println!("{:?}", t)
    }
}

struct MyWrapper<T> {
    inner: Box<dyn MyTrait<T>>
}

impl<T> MyWrapper<T> {
    pub fn new(inner: Box::<dyn MyTrait<T>> ) -> Self { 
        Self { inner } 
    }
}

impl<T> MyTrait<T> for MyWrapper<T> {
    fn my_func(&self, t: T) {
        self.inner.my_func(t);
    }
}

fn foobar() {
    let the_impl = MyImpl{};        
    //let the_impl = MyWrapper::new(Box::new(the_impl)); // (*) 
    
    for entry in walkdir::WalkDir::new("blah") {
        let entry = entry.unwrap(); 
        let path = entry.path(); // <== here
        the_impl.my_func(path);
    }
}

When the line marked (*) is commented, everything is fine. However, if uncommented, the compiler complains about entry not living long enough, see the line marked "here".

I fail to understand how the wrapper happens to change the way the path is borrowed.

EDIT

As pointed out by @Jmb below, this has nothing to do with Path, and the same problem arises with a simple &str, for example:

impl MyTrait<&str> for MyImpl {
    fn my_func(&self, t: &str) {
        println!("{:?}", t)
    }
}

fn foobar_str() {
    let the_impl = MyImpl{};        
    let the_impl = MyWrapper::new(Box::new(the_impl));
    {
        let s = String::from("blah").clone();
        the_impl.my_func(&s as &str); // <== same error
    }
}

Upvotes: 5

Views: 161

Answers (2)

fbilhaut
fbilhaut

Reputation: 76

Although the previous answer and comments provide very useful insights about lifetime inferences in this particular case, they don't come with a practical solution.

I finally found out the following one. First let's simplify a bit the problem, using a String for now:

trait MyTrait<T> { fn my_func(&self, t: T); }

struct MyImpl {}

impl MyTrait<&String> for MyImpl {
    fn my_func(&self, t: &String) { println!("{}", t) }
}

struct MyWrapper<T> { inner: Box<dyn MyTrait<T>> }

impl<T> MyTrait<T> for MyWrapper<T> {
    fn my_func(&self, t: T) { self.inner.my_func(t); }
}

Of course it fails exactly for the same reason as before:


fn foobar() {
    let the_impl = MyImpl{};
    let the_impl = MyWrapper { inner: Box::new(the_impl) };
    {
        let s = String::from("blah");
        the_impl.my_func(&s); // <== error: 's' does not live long enough
    }
}

However, if one changes MyTrait so T is passed by reference in the signature of my_func, and adapts the rest accordingly:

trait MyTrait<T> { fn my_func(&self, t: &T); } // <== main change here

struct MyImpl {}

impl MyTrait<String> for MyImpl {
    fn my_func(&self, t: &String) { println!("{}", t) } // <== note the actual signature hasn't changed
}

struct MyWrapper<T> { inner: Box<dyn MyTrait<T>> }

impl<T> MyTrait<T> for MyWrapper<T> {
    fn my_func(&self, t: &T) { self.inner.my_func(t); }
}

Then the foobar() function can be left unchanged, but now it compiles.

And, as stated by @kmdreko below, it will also work for str or or other non-sized types like Path, with the following modifications:

trait MyTrait<T: ?Sized> { fn my_func(&self, t: &T); }

struct MyWrapper<T: ?Sized> { inner: Box<dyn MyTrait<T>> }

impl<T: ?Sized> MyTrait<T> for MyWrapper<T> {
    fn my_func(&self, t: &T) { self.inner.my_func(t); }
}

Then, to come back to the initial use case, the following code now works as expected:

impl MyTrait<Path> for MyImpl {
      fn my_func(&self, t: &Path) { println!("{:?}", t) }
}

fn foobar_with_path_in_a_loop() {
    let the_impl = MyImpl{};        
    let the_impl = MyWrapper { inner: Box::new(the_impl) };
    
    for entry in walkdir::WalkDir::new("blah") {
        let entry = entry.unwrap();
        let path = entry.path();
        the_impl.my_func(path);
    }
}

Bottomline

See @Jmb's answer and associated comments for some explanations about why the first solution doesn't compile.

Upvotes: 0

Jmb
Jmb

Reputation: 23463

path has type &'a Path for some lifetime 'a that is only valid for a single loop iteration (until entry gets dropped).

When wrapped in your wrapper, the_impl has type MyWrapper<T> for some inferred type T.

Since you call the_impl.my_func (path) the compiler infers that T == &'a Path, so the_impl has type MyWrapper<&'a Path>, which can't exist for longer than the 'a lifetime. Hence the error since the_impl needs to exist for the whole loop.

When you don't wrap the_impl, it has type MyImpl which implements MyTrait<&'b Path> for all lifetimes 'b, including lifetimes that are shorter than the lifetime of the_impl (they only need to be long enough for the call to my_func). So the compiler can use the MyTrait<&'a Path> implementation, without affecting the lifetime of the_impl.

There is nothing special with the Path type here, but there may be with your &str implementation. I suspect that in the latter case you wound up with &'static str, which can live forever if needed.

Upvotes: 4

Related Questions