Search code examples
listpolymorphismrustselftraits

Issue with Rust using dynamic polymorphism on trait when specifying lifetime on self


I have finally decided to give Rust (1.7 & 1.8) a try. Coming from C++, I must say Rust looks awesome. I was trying to reproduce a well known behavior in C++ that consists of using dynamic polymorphism on a set of objects.

I have been through a very annoying problem that I was able to solve, but I would like to know what you guys think of that issue, and I hope it might help others that are trying to do the same kind of thing.

Let's consider the following code that implements the initial idea:

struct Foo
{
    foo: u32,
}
trait Bar
{
    fn bar(& self) -> u32;
}
impl Bar for Foo
{
    fn bar(& self) -> u32 
    {
        return self.foo; 
    }
}
fn foobar(l: &std::collections::LinkedList<& Bar>)
{
    for i in l
    {
        println!("{}", i.bar());
    }
}
fn main()
{
    let foo0 = Foo{foo: 8u32};
    let foo1 = Foo{foo: 8u32};
    let mut l = std::collections::LinkedList::new();
    l . push_back(&foo0 as &Bar);
    l . push_back(&foo1 as &Bar);
    foobar(&l);
}

Here everything is compiling, everything is working perfectly.

I wanted to pass another reference to the function bar of the trait Bar, as a result I had to add a lifetime to the Bar trait. To keep it simple, I will just add the lifetime (as it will compile nicely under Rust 1.8). After adding the lifetime through the whole code, the code eventually looks like this:

struct Foo
{
    foo: u32,
}
trait Bar<'a>
{
    fn bar(& 'a self) -> u32;
}
impl<'a> Bar<'a> for Foo
{
    fn bar(& 'a self) -> u32 
    {
        return self.foo; 
    }
}
fn foobar<'a>(l: &std::collections::LinkedList<& 'a Bar>)
{
    for i in l
    {
        println!("{}", i.bar());
    }
}
fn main()
{
    let foo0 = Foo{foo: 8u32};
    let foo1 = Foo{foo: 8u32};
    let mut l = std::collections::LinkedList::new();
    l . push_back(&foo0 as &Bar);
    l . push_back(&foo1 as &Bar);
    foobar(&l);
}

If you compile this code, the error message will be the following:

../test.rs:21:12: 21:13 error: cannot infer an appropriate lifetime due to conflicting requirements [E0495]
../test.rs:21   for i in l
                         ^
../test.rs:19:1: 25:2 help: consider using an explicit lifetime parameter as shown: fn foobar<'a>(l: &std::collections::LinkedList<&'a Bar>)
../test.rs:19 fn foobar<'a>(l: &std::collections::LinkedList<& 'a Bar>)
../test.rs:20 {
../test.rs:21   for i in l
../test.rs:22   {
../test.rs:23     println!("{}", i.bar());
../test.rs:24   }
              ...
error: aborting due to previous error

Here the problem is clear, the compiler knows that the variables given in push_back have different lifetimes, hence it cannot be compliant and disagree with what I have written. The problem can be solved if the variables foo0 and foo1 are declared in the same let statement.

I found it very hard to figure out what was wrong in that code. My questions are:

  • Is there a way to tell the compiler that a collection might take different lifetime?

  • By setting the 'a lifetime on the other reference variable (that does not appear here) instead of self, the code compiles. Does that mean if we do not specify any lifetime then the compiler puts the '_ that correspond to the specific lifetime I am asking in my previous question?

  • Is there an implicit rule that "forbids" us to set a lifetime on self?

  • Is that piece of code idiomatic Rust?


Solution

  • The second code sample will compile if you change the definition of foobar to this:

    fn foobar<'a>(l: &std::collections::LinkedList<&'a Bar<'a>>) {
        for i in l {
            println!("{}", i.bar());
        }
    }
    

    However, what you're doing with lifetime parameters doesn't make a lot of sense to me.

    We usually define a trait that is generic over a lifetime (such as your second version of Bar) when we want to have a method that returns a reference whose lifetime is the lifetime of a member of the implementor. For example, suppose we have the following struct:

    struct Foo<'a> {
        foo: &'a str,
    }
    

    This struct contains a reference, and we must explicitly name that lifetime. We want to allow any lifetime, so we define a lifetime parameter, 'a.

    We can add an inherent method to this struct:

    impl<'a> Foo<'a> {
        fn foo(&self) -> &'a str {
            self.foo
        }
    }
    

    Notice how &self doesn't have an explicit lifetime. Instead, we use the 'a parameter from the impl on the return type of the foo method, since we want the lifetime of the returned value to be the same as the lifetime in the struct, not the lifetime of self (which would usually be shorter).

    What if we wanted to define a trait method similarly?

    trait Bar<'a> {
        fn bar(&self) -> &'a str;
    }
    
    impl<'a> Bar<'a> for Foo<'a> {
        fn bar(&self) -> &'a str {
            self.foo
        }
    }
    

    The trait Bar defines a lifetime parameter and the impl links it to the lifetime parameter of Foo.

    Instead of adding lifetime parameters on the trait, you can also add lifetime parameters on individual methods too.

    For example, consider this trait:

    trait Slice {
        fn slice<'a>(&self, s: &'a str) -> &'a str;
    }
    

    We want the result to have the same lifetime as the s parameter. To do this, we need to define a lifetime parameter on the method and apply it on both references.

    For completeness, here's an implementation of that trait:

    struct SliceBounds {
        start: usize,
        end: usize,
    }
    
    impl Slice for SliceBounds {
        fn slice<'a>(&self, s: &'a str) -> &'a str {
            &s[self.start..self.end]
        }
    }