Downcasting non-'static types


#1

Why is it that we’re only able to downcast 'static types? (Any requires 'static)

I understand that lifetimes are erased, so it’s not possible to check at runtime whether a certain type has the expected lifetime, but if I can prove at compile time that my type is : 'a, why I am not allowed to downcast to T<'a>? In other words, why would the following be unsound?

// assuming `Any` does not require `'static`
// and has a lifetime parameter instead
impl<'a> Any<'a> {
    fn downcast_ref<T: 'a>(&self) -> Option<&T> { ... }
}

impl<'a, T: 'a> Any<'a> for T {}

Note: the alternative without a lifetime parameter,

impl Any {
    fn downcast_ref<'a, T: 'a>(self: &'a Any) -> Option<&'a T>;
}

is unsound because of contravariance: one could cast &fn(&'static str)&'static Any&'a Any&'a fn(&'a str); while trait objects are, I believe, invariant over their lifetime parameters (since PhantomFn was removed), so doing something like this is impossible.


#2

&'a i32 and &'static i32 will have the same TypeId (because lifetimes are erased at runtime) and both have the : 'a bound. Therefore you’ll be able to upcast &&'a i32 to &Any<'a> and then downcast it to &&'static i32. As references are Copy it allows conversion from &'a i32 into &'static i32 and it’s apparently unsound.

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TypeId(u64);

impl TypeId {
    pub fn of<T: ?Sized>() -> Self {
        unimplemented!();
    }
}

pub trait Any<'a>: 'a {
    fn get_type_id(&self) -> TypeId;
}

impl<'a, T: ?Sized + 'a> Any<'a> for T {
    fn get_type_id(&self) -> TypeId {
        TypeId::of::<Self>()
    }
}

impl<'a> Any<'a> {
    pub fn downcast_ref<T: 'a>(&self) -> Option<&T> {
        if self.get_type_id() == TypeId::of::<T>() {
            Some(unsafe { &*(self as *const Self as *const T) })
        } else {
            None
        }
    }
}

fn frob(x: i32) -> &'static i32 {
    let xref: &i32 = &x;
    let xrefref: &Any = &xref;
    *xrefref.downcast_ref::<&i32>().unwrap()
}

However, Any<'a> is not the only way to achieve non-static downcasting. The 'static bound on type_id is removed to test such safe variants of non-static downcasting.


#3

So the actual problem is that T: 'a guarantees that T lives at least for 'a, not at most for 'a, and there’s no way to express the latter. Thanks!

The RFC you’ve linked actually contains a discussion about this and a suggestion very similar to mine above, which was rejected for this very reason.