Returning a concrete type as a generic

I want to do some very limited specialization for some structs that I’m writing. In particular, I want to define a function that takes a type parameter T that represents a desired return type, and be able to construct some of the requested types by specialized methods and others by a generic fallback; downstream code should not need to be modified when new special cases are added.

A lot of this is fairly straightforward with TypeId and Any, but I had trouble figuring out how to return a T that I had constructed as a concrete type after checking type equality. I eventually landed on the code below, and have 3 questions:

  • Does this code work correctly and maintain all of the relevant safety rules?
  • Is there a better way to achieve my goals in stable Rust?
  • Does it make sense to add a method similar to this into std::any somewhere?
use std::any::{TypeId,Any};
use std::ptr;

#[inline(always)]
fn downcast<In:Any, Out:Any>(x:In)->Option<Out>
{ if TypeId::of::<In>() == TypeId::of::<Out>()
  { unsafe { Some(ptr::read(&mut std::mem::ManuallyDrop::new(x)
                            as *mut std::mem::ManuallyDrop<In>
                            as *mut Out                         )) } }
  else { None }
}

#[test] fn f()
{ let mut x:Option<i32>;
  x = downcast(4i32);
  assert_eq!(x.unwrap(), 4);
  x = downcast(4f64);
  assert!(x.is_none());
}

(Playground)

I think you need a bound on In: 'static and Out: 'static to make this fully sound. TypeId doesn't take lifetimes into account, so right now your code allows creating, say, &'static str from &'a str.

Besides that, I'm slightly concerned about that big unsafe one-liner. It's not entirely clear to me how long everything lives, and if at any point two mutable references exist pointing to the same bit of memory, you get instant UB. It might be OK as is, but I can't really tell if that's the case. In particular, I'm slightly concerned about a ManualDrop<In> and Out existing which point to the same data (what if the type is a mutable reference of some sort?).

What would you think of rewriting it slightly more explicitly like this?

        let x = std::mem::MaybeUninit::new(x);
        let res = x.as_ptr() as *mut Out;
        unsafe {
            Some(std::ptr::read(res))
        }

This way it's very clear that x lasts long enough to read from, and we won't get any dangling pointers. As for MaybeUninit, I would just be more comfortable asserting that this is valid - ManualDrop might also work fine, but it has slightly more guarantees, and with MaybeUninit it should 100% be fine.

Yes, this is sound because Any requires 'static. I strongly recommend that you look into rustfmt, because the code is pretty difficult to read as is.

1 Like

It might be sound, but it seems to be in very bad taste to reimplement something dangerous like this. Why don't you use one of the many downcast() methods already implemented in the standard library?

Furthermore, I'd first of all ask why you need downcasting. Usually it has a very limited and very specific set of valid use cases, and in the overwhelming majority of the time, it indicates a design/architectural error in the code.

1 Like

I’m curious: Is there a performance benefit over the following – already safe – alternative?

fn downcast<In: Any, Out: Any>(x: In) -> Option<Out> {
    (&mut Some(x) as &mut dyn Any)
        .downcast_mut()
        .and_then(|y: &mut Option<Out>| y.take())
}
4 Likes

Because all of those work with references of some sort or another, and not owned types. I want to implement MyFrom<R:Record> for ConcreteType, where ConcreteType implements the Record trait (From would be better, but the blanket self-impl gets in the way). When Self is R, I don't want to go through the hassle of constructing a new instance piece-by-piece.

ManuallyDrop was recommended as a better alternative by the mem::forget docs: https://doc.rust-lang.org/stable/std/mem/fn.forget.html#relationship-with-manuallydrop

I hadn't thought of MaybeUninit because the value is definitely initialized, I just want to take ownership in a way that the compiler recognizes.

@steffahn: I don't know whether or not that would get optimized out by the compiler after monomorphization.

For my original version, at least after the const_type_id feature stabilizes, the conditional should go away. Hopefully the compiler would also be able to optimize away all of the pointer silliness and end up with effectively the same code as the theoretical specialization:

fn downcast<In:Any, Out:Any>(_: In)->Option<Out> { None }
fn downcast<In:Any, Out=In >(x: In)->Option<Out> { Some(x) }

In any case, I'm not aiming for massive performance here, and your version is easier to verify is correct.

1 Like

Optimization seems to get rid of everything after monomorphization no matter what: https://godbolt.org/z/KHPr7G

3 Likes

Also, with specialization you won’t need the Any trait anymore for this:

#![feature(specialization)]

trait Cast<Out>: Sized {
    fn cast(self) -> Option<Out>;
}
impl<In, Out> Cast<Out> for In {
    default fn cast(self) -> Option<Out> {
        None
    }
}
impl<In> Cast<In> for In {
    fn cast(self) -> Option<In> {
        Some(self)
    }
}

fn downcast<In, Out>(x: In) -> Option<Out> {
    // x.cast()
    // ^ this ought to work, however specialization is
    // still a bit buggy, so let’s be more explicit:
    Cast::<Out>::cast(x)
}

#[test]
fn f() {
    let mut x: Option<i32>;
    x = downcast(4i32);
    assert_eq!(x.unwrap(), 4);
    x = downcast(4f64);
    assert!(x.is_none());
}

Perhaps that was what you were referring to before. I guess this code could lead to confusing results around types with non-matching lifetimes though.

More or less; I like staying on stable if possible and specialization seems like it could be a while, so I decided to see what's possible today.

I'm actually using downcast to fake specialization for other methods. As long as Any (and therefore 'static) is an acceptable constraint and you can list all of the options together, it doesn't work too badly.

Regarding this: You can actually rewrite the Any constraints into just 'static and it still compiles. Perhaps that was clear to you anyways. I’m also not sure if this wouldn’t decrease readability—functionality-wise the signatures would be equivalent.

fn downcast<In: 'static, Out: 'static>(x: In) -> Option<Out> {
    (&mut Some(x) as &mut dyn Any)
        .downcast_mut()
        .and_then(|y: &mut Option<Out>| y.take())
}
1 Like

Right; I assume that Any semantically means "a type that can be reasoned about at runtime," and its definition will be updated alongside any language changes that alter that set. In particular, if Any and TypeId are ever upgraded to reason about lifetimes, I'd like that to be properly reflected here.

I use 'static bounds when I care about memory actually remaining valid over time, which I don't here.

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.