Lifetime requirement of `Any` as supertrait

I'm struggling a bit to compile the following:
Given some trait Foo: Any some structs that implement including Bar, and a vector: Vec<Box<dyn Foo>>.

Now I want to filter the vector based on a function from Foo which requires to know which previous elements passed the filter.
As a quick test I came up with the following using only the Any trait which works but isn't exactly what I need (since I need Foo):

fn test1(list: &[&dyn Any]) -> bool{
    list.iter().filter(|b| b.is::<Bar>()).count() == 0
}

I thought I could now simply replace every Any with Foo, but that doesn't work and gives me some lifetime issues regarding 'static lifetime requirements of Any:

use core::any::Any;

trait Foo: Any{}
struct Bar{}
impl Foo for Bar{}
impl Foo for i8{}


fn test1(list: &[&dyn Any]) -> bool{
    list.iter().filter(|b| b.is::<Bar>()).count() == 0
}

fn test2(list: &[&dyn Foo]) -> bool{
    list.iter().filter(|b| b.is::<Bar>()).count() == 0
}

fn main(){
    let list: Vec<Box<dyn Foo>> = vec![
        Box::new(1i8),
        Box::new(Bar{}),
        Box::new(2i8),
        Box::new(Bar{}),
    ];
    
    let mut temp = vec![];
    for item in list.iter(){
        //if !test1(&temp) {
        //    temp.push(item.as_ref());
        //}
        if !test2(&temp) {
            temp.push(item.as_ref());
        }
    }
}

playground

Is it possible to somehow work around this?

I guess I could implement the Any::is function in Foo aswell, but I wouldn't exactly consider this a wonderful solution..

Rust doesn't automatically support casting from one trait object type to another (for example, from dyn Foo to dyn Any) because the vtables for trait objects don't provide the information needed to do this. This might be added in the future, but for now you can provide your own conversion function, e.g.:

trait Foo: Any{
    fn as_any(&self) -> &dyn Any;
}
struct Bar{}
impl Foo for Bar {
    fn as_any(&self) -> &dyn Any { self }
}
impl Foo for i8 {
    fn as_any(&self) -> &dyn Any { self }
}

And you can use it like this:

fn test2(list: &[&(dyn Foo + 'static)]) -> bool{
    list.iter().filter(|b| b.as_any().is::<Bar>()).count() == 0
}

The 'static bound is necessary because of how Any is implemented: The compiler generates unique TypeIds for each type at compile time, but it can't do this for types that contain lifetime parameters, because it can't enumerate all possible lifetimes at compile time.

(As you can see, trait objects in Rust are a very leaky abstraction: Details of the implementation often have a direct effect on what you are allowed to do with them. This can sometimes make them feel unintuitive or cumbersome.)

2 Likes

It's possible, but you're going to run into more problems, I'm afraid.

The main problem is that is is a method impl on dyn Any, not part of the trait. If you look at it's documentation, you can see that it's in impl dyn Any: Any in std::any - Rust

The reason you get lifetime errors, then, is because it isn't treating dyn Foo as dyn Any. Rather it's trying to cast one of the other layers into a dyn Any: it's trying to go from &&&dyn Foo into &dyn Any, and the fact that the first and second layers of references (introduced by filter, and iter respectively) are temporary.

If you change the code to unpack those (and call is directly on &dyn Any), you'll see a different error:

fn test2(list: &[&dyn Foo]) -> bool{
    list.iter().filter(|&&b| Any::is::<Bar>(b)).count() == 0
}
error[E0308]: mismatched types
  --> src/main.rs:14:45
   |
14 |     list.iter().filter(|&&b| Any::is::<Bar>(b)).count() == 0
   |                                             ^ expected trait `std::any::Any`, found trait `Foo`
   |
   = note: expected reference `&(dyn std::any::Any + 'static)`
              found reference `&dyn Foo`

(playground)

Now, as for fixing it, you have a few options - but there isn't a single really "good" one. Any::is is, well, a method only available if you have a dyn Any - not dyn SomeOtherTrait. To make matters worse, rust has no way to cast to supertraits - unlike languages like Java, there's no inherent way to go from dyn Any to dyn SomeOtherTrait - the vtable ptr needed to create dyn Any literally isn't stored in dyn Foo.

To fix this, you need to have a method which creates a dyn Any from your dyn Foo, and is somehow recompiled for each instance of dyn Foo. The easiest way I've found to do this is to use an auxiliary trait which is auto-implemented for everything concrete, and then to require it as a dependency to bring it into the trait object:

trait Foo: Any + FooExt {}
trait FooExt: Any { // requires same traits as Foo
    fn as_any(&self) -> &dyn Any;
}
impl<T> FooExt for T where T: Foo + Sized { // but it's only implemented if also Sized
    fn as_any(&self) -> &dyn Any {
        self
    }
}

(playground with this impl)

This is a bit tricky, but it should put a limited burden on implementors of the trait, while still offering functionality. I would recommend sticking it in a default method in Foo itself, but unfortunately you can't do this cast without the Self: Sized bound, and I don't think there's a way to stick the implementation inside Foo itself without rust then complaining when you make dyn Foo trait objects. By using the separate trait, Foo itself doesn't have to care at all that the implementation requires Sized. It only cares that something provides the as_any function, so it can still exist as dyn Foo. It'll end up requiring extra effort to implement Foo for anything which isn't Sized, but I think that's reasonable as those types won't necessarily have any way to conver to dyn Any in any case.

Hope that helps! I think something like this could really end up useful as a utility library, and one probably exists, but I don't know where it is. I've written a number of weird hacks like this, but they all end up being weird and specific - so making utility crates rarely seems worth it.

Edit: looks like my answer overlaps with @mbrubeck's a bit! Leaving it as is since I think both are probably helpful.

2 Likes

The crates downcast, downcast-rs, and MOPA (my own personal any) deserve mention here as providing helpers for this pattern. downcast-rs seems to be the most featureful of the three options.

The downside of using one of these helper libraries rather than cooking your own (as it were) is that they then become public dependencies, and thus part of your library's API. It's up to you whether they're worth it.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.