Downcast for a type with bounded lifetime?

Hello,

Is there something like the Any trait that allows for the type to have a bounded lifetime, but still permits down-casting back to the original type?

use core::any::Any;
use std::collections::HashMap;

struct BucketOJunk<'a> {
    map : HashMap<&'static str, Box<dyn Any + 'a>>
}

fn main() {
    let mut bucket_o_junk = BucketOJunk{ map: HashMap::new()};

    bucket_o_junk.map.insert("elephant", Box::new("Elephant".to_string()));
    bucket_o_junk.map.insert("forty two", Box::new(42));
    
    let old_shoe = "Old Shoe".to_string();
    bucket_o_junk.map.insert("old shoe ref", Box::new(&old_shoe));

    println!("{}", bucket_o_junk.map.get("forty two").unwrap().downcast_ref::<i32>().unwrap());
    println!("{}", bucket_o_junk.map.get("old shoe ref").unwrap().downcast_ref::<&String>().unwrap());
}

Thanks in advance.

Besides defining your own Any trait that has a dedicated method for each type you need, no.

trait MyAny {
    fn try_into_i32(&self) -> Option<i32> {
        None
    }
    fn try_into_str(&self) -> Option<&str> {
        None
    }
}

impl MyAny for i32 {
    fn try_into_i32(&self) -> Option<i32> {
        Some(*self)
    }
}
impl<'a> MyAny for &'a str {
    fn try_into_str(&self) -> Option<&str> {
        Some(*self)
    }
}
2 Likes

Thanks for the reply. In my situation anticipating the types is not really possible as the types are essentially user-supplied generics. But I can't use a generic type parameter because of the "bag_o_junk" approach where the same object needs to hold various types.

I was thinking through whether I could do something with unsafe. Basically have a PhantomData ride along side the Any, so the lifetime wouldn't be violated, and then use unsafe to coerce it to 'static.

This seemed like it would work, but if it were so simple then the Any trait wouldn't have a static bound. So clearly there is a nuance I'm not seeing.

Thanks for the reply.

If you know for sure that the type is correct, you can just take a &dyn YourTrait and convert it to the right type by converting to a raw pointer, casting it, and going back to a reference. The reason that Any does not work for non-'static constructs is that it needs a way to check whether the type is right, but if two types differ only in their lifetimes, they have the same type id.

The type-checking behavior of Any is a nice feature. Since I'm exposing it to the API client, I'd definitely rather panic than do UB if an attempt to downcast to the wrong type is made.

I know I'm being dense but I don't understand why a Box<dyn Any + 'a> isn't valid. (I understand Any is declared with : 'static) but I don't understand why it wasn't designed to accept a lifetime bound just like all other types.

I'm trying to imagine the situation where I could use a theoretical dyn Any + 'a to expand the lifetime of an object in an invalid way, but I'm not seeing it. But if I've learned anything at all from users.rust-lang.org, it's that y'all are much smarter than me about this kind of thing, so I should trust that the Rust designers put the 'static bound there because there was no other choice.

Imagine if you took an &'a str and put it into a Box<dyn Any + 'a>. Then you try to downcast that to a &'static str, which succeeds since their type ids are the same. Now you have a &'static str that doesn't live forever.

Ahhhh. I failed to clarify, in my perfect theoretical world, the downcast_ref method would pass the lifetime bound from the Any object to the resulting object.

But I guess I'm starting to see how that's a can-of-worms as it's not clear what part of the resulting object the bound belongs on.

So an Any +'a that could downcast_ref() -> &'a ???? could totally work, but it would need a different method to downcast_ref() -> ????<'a>

As long as the only lifetime appears on the reference itself, you can use the &'a dyn Any type to do that, instead of the box. The pointed-at type must still be 'static though. There's no way to do it for ????<'a>.

Unfortunately downcast_ref() -> ????<'a> is my real use-case. I'm starting to get how making this work is actually really hard - given lifetimes are a compile-time construct and downcast_ref() is evaluated at runtime...

I wonder if there is precedent anywhere else in the language for capturing lifetime information for use at runtime...

Anyway, thanks for all your insights.

There is none, all lifetime info is erased before codegen, so nothing related to lifetimes exists at runtime.

I have recently asked a question on IRLO that received some pretty detailed answers as to why this is not possible, or at least massively impractical.

I needed something like this recently, when I wanted to have downcastable Send types have access to some kind of !Send storage when they're present on one particular thread. I came up with something like this:

use std::any::{Any,TypeId};

pub trait Opaque {}
impl<T> Opaque for T {}

pub struct AssocBox<'a> {
    typeid: TypeId,
    data: Box<dyn Opaque + 'a>
}

pub trait HasAssoc<'a>: Any {
    type Assoc: Sized + 'a;
    // Probably some kind of constructor here that takes `&'a Something`
    // and returns Self::Assoc
}

impl<'a> AssocBox<'a> {
    pub fn new<T:HasAssoc<'a>>(data: T::Assoc)->Self {
        AssocBox {
            typeid: TypeId::of::<T>(),
            data: Box::new(data)
        }
    }
    
    pub fn downcast_ref<T:HasAssoc<'a>>(&self)->Option<&T::Assoc> {
        if TypeId::of::<T>() == self.typeid {
            unsafe { Some(&*(
                self.data.as_ref() as *const (dyn Opaque + 'a)
                                   as *const T::Assoc
            ))}
        } else {
            None
        }
    }
}

(This is from memory, and likely has some problems)

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.