Issue casting between type parameters <T, U>


#1

Hello,

This one has been troubling me for a while: For some reason rust does not seem to support casting between type parameters <T, U> in a function where an ‘as’ cast would work in the same function if U was hand monomorphized out.

Version with two type parameters that fails:

use std::vec::Vec;

pub trait GetterTrait<ET> where ET: ?Sized {
    fn get_val_as_trait( & self, element_index: usize) -> &ET;
}

pub trait ElementTrait {}

struct A;

pub struct Foo<T> {
    pub val: T,
}

impl ElementTrait for A{}

impl <T, ET> GetterTrait<ET> for Foo<T>
    where T: ElementTrait + Sized,
          ET: ElementTrait + ?Sized + 'static {

    fn get_val_as_trait( &self, element_index: usize) -> &ET {

        // error[E0605]: non-primitive cast: `&mut T` as `&mut ET`
        // This cast 'should' work because T is a sized object such as a struct
        // and ET is an un-sized object such as a Trait.
        let element: &ET = &self.val as &ET;

        element
    }
}

fn main() {

}

Hand monomorphized version where U is hand monomorphised to ElementTrait. This version no longer fails.

use std::vec::Vec;

pub trait GetterTrait {
    fn get_val_as_trait( & self, element_index: usize) -> &ElementTrait;
}

pub trait ElementTrait {}

struct A;

pub struct Foo<T> {
    pub val: T,
}

impl ElementTrait for A{}

impl <T> GetterTrait for Foo<T>
    where T: ElementTrait + Sized {

    fn get_val_as_trait( &self, element_index: usize) -> &ElementTrait {

        // This works perfectly fine now after I hand monomorphized it
        let element: &ElementTrait = &self.val as &ElementTrait;

        element
    }
}

fn main() {

}

I feel like the issue here is that rust is performing some checks and making some assumptions about the type parameters that are wrong prior to monomorphization.

My workaround to this issue is to use macro rules to essentially force a sort of macro based monomorphization but it seems a bit awkward to need to reach for that in this case. Any ideas?


#2

In example #1, you have the following type parameters: <T, ET> and their respective type bounds. T and ET could either be structs or traits, and because of that, there is no guarantee that ET will just end up being an ElementTrait or something higher on the inheritance chain (I.E., if ElementTrait: Debug, then you could cast a struct B which impl ElementTrait for B in let _: &dyn Debug = &B::new()) and so you could either be casting to a struct that inherits ElementTrait (Which isn’t allowed because they’re different structs) or a trait object (Which is allowed)

In example #2 you explicitly specify that you need to cast to a trait object that either is or is above ElementTrait


#3

Thanks for the insights,

That does make more sense to me now. I was also wondering why the compiler just couldn’t wait to analyse what concrete types a user actually uses and only check if casting can occur after monomorphization has taken place instead of before. However, that would create a poor user experience because the user would not know that they are meant to use a trait where U is until they try sticking a struct there and the compiler complains.

Which makes me think it would be nice for the language to provide a way to tag a type paramater as being a Trait to take that OR out of the equation and prevent the compiler from consider that U could ever be a struct. Maybe a nice little ergonomic language improvement?

Anyway for now i am happy that I know why it was getting tripped up.

Thanks again


#4

There’s actually no way to have something generic over a trait in current Rust. There are only two generic kinds: types, and lifetimes.

If Trait is object safe, you can use Trait as shorthand for the type dyn Trait (the type of a bare trait object of Trait). That’s what &ElementTrait is: it’s the old way of writing &dyn ElementTrait. (In time, the version without dyn will be deprecated and hopefully removed, so you should always write dyn when you’re referring to the type.)

ET: ?Sized relaxes the default Sized bound on generic types so that dyn ElementTrait is a valid type to substitute for ET. But it doesn’t let you write code that is generic over traits. For that, we really would need a language change.

Knowing this, you can use the unstable unsize feature to write a bound that means "T can be unsized to ET when behind a pointer of some kind":

#![feature(unsize)]

impl<T, ET> GetterTrait<ET> for Foo<T>
where
    T: std::marker::Unsize<ET>,
    ET: ElementTrait + ?Sized,
{
    fn get_val_as_trait(&self, _element_index: usize) -> &ET {
        let element: &ET = &self.val as _;
        element
    }
}

However, I don’t think you can upcast &dyn Subtrait to &dyn ElementTrait even if ElementTrait is a supertrait of Subtrait, so this may not be good enough for your real use case.


#5

Thank you so much trentj! What a brilliant new years gift :smile: I had come across Unsize in my wanderings through nightly code but thought it was just an alias for ?Sized. But now you have enlightened me and have potentially allowed me to convert my 169 lines of macro code (In my real use case) to generic code.

As for up-casting, ElementTrait is actually the lowest level trait that I am working with here, which makes me think I should re-factor the code to indicate that more clearly if possible.