genericsrustspecialization

How to cast concrete type to generic (manual specialization)


I'm writing some code and I would like to wrap common functionalities in one place. So here's the code

pub struct Foo { }

pub fn process_foo() -> Result<Foo, X>;
pub fn process_usize() -> Result<usize, X>;

Now I could keep those functions separate and deal with it, but I find it a bit ugly. So I wanted to wrap them around in a generic:

pub fn process<T>() -> Result<T, X>;

Of course Rust doesn't support specialization yet (at least on stable channel) and so I cannot really specialize this method. But I don't have many types, so I came up with the following workaround:

pub enum TypeInfo {
    Usize,
    Foo,
}

pub trait WithTypeInfo {
    fn type_info() -> TypeInfo;
}

impl WithTypeInfo for Foo {
    fn type_info() -> TypeInfo { TypeInfo::Foo }
}

impl WithTypeInfo for usize {
    fn type_info() -> TypeInfo { TypeInfo::Usize }
}

and I've modified my process<T> to allow T: WithTypeInfo only. With this I have:

pub fn process<T: WithTypeInfo>() -> Result<T, X> {
    match T::type_info() {
        TypeInfo::Foo => process_foo(),
        TypeInfo::Usize => process_usize(),
    }
}

This of course doesn't compile, because Rust doesn't know how to convert Result<Foo, X> to Result<T, X> and Result<usize, X> to Result<T, X>. Even though I've guaranteed myself that in both cases these are the same types.

So how to deal with it? Well, the only option I found is through an unsafe macro:

pub fn process<T: WithTypeInfo>() -> Result<T, X> {

    macro_rules! mutate {
        ( $e:expr ) => {
            {
                let item = { $e }?;
                let src = core::ptr::from_ref(&item).cast::<T>();
                let dst = unsafe { core::ptr::read(src) };

                #[allow(forgetting_copy_types, clippy::forget_non_drop)]
                {
                    std::mem::forget(item);
                }

                Ok(dst)
            }
    };

    match T::type_info() {
        TypeInfo::Foo => mutate!(process_foo()),
        TypeInfo::Usize => mutate!(process_usize()),
    }
}

This works. So what this macro does is... pretty much nothing. It takes object and reinterprets it as T. I needed to add std::mem::forget(item); because I ended up with two copies of the same object, one had to be forgotten. And in my real scenario some of those objects have proper Drop implementation.

I've run some tests, and it works.

Is that an ok design? Is there any flaws that I've missed? Or is there a better way to achieve what I want?


Solution

  • You don't need specialization unless you specialize something, obviously. In other words, unless you have a default implementation for all types other than X.

    Since you only want to list some types, this is easily supported with normal traits:

    pub trait Process: Sized {
        fn process() -> Result<Self, X>;
    }
    
    impl Process for Foo {
        fn process() -> Result<Self, X> {
            process_foo()
        }
    }
    
    impl Process for usize {
        fn process() -> Result<Self, X> {
            process_usize()
        }
    }
    
    pub fn process<T: Process>() -> Result<T, X> {
        T::process()
    }