Type coercion will cause underlying `TypeId` changed

I believe someone has asked this question, but I haven't found it yet.
Let's look at this example:

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

pub struct MyData<T: ?Sized> {
    id: TypeId,
    data: T,
}

impl<T: ?Sized + 'static> MyData<T> {
    pub fn new(t: T) -> Self
    where
        T: Sized,
    {
        MyData {
            data: t,
            id: TypeId::of::<T>(),
        }
    }

    // this may returns false
    pub fn type_id_eq(&self) -> bool {
        self.id == TypeId::of::<T>()
    }
}

type_id_eq seems should always be true, but that situation is limited to the type T not changed. We can do type coercion to change the type forcedly, then the conflict appears.

fn main() {
    let origin = Box::new(MyData::new("aaa".to_string()));
    let new_type = origin as Box<MyData<dyn AsRef<str>>>;
    assert!(!new_type.type_id_eq());
}

Can I both prevent the outside code from coerce the type and reserve a method to change the type in my specified way?

1 Like

I don't think so, other than making type-changing coercions impossible (by not supporing ?Sized for the unsizing cases, say).

Note that there is also subtyping to consider (though you could make the parameter invariant to prevent this one).

    let origin: MyData<fn(&str)> = MyData::new(|_| {});
    let new_type: MyData<fn(&'static str)> = origin;
    assert!(! new_type.type_id_eq());
2 Likes

Thanks, I didn't expect subtyping. If just only to prevent coercion, this may be a better way, which can make T invariant.

pub struct MyData<T: ?Sized> {
    id: TypeId,
    _p: PhantomData<fn(T) -> T>,
    data: T,
}

Then subtyping is not allowed.

error[E0308]: mismatched types
  --> src/main.rs:28:46
   |
28 |     let new_type: MyData<fn(&'static str)> = origin;
   |                                              ^^^^^^ one type is more general than the other
   |
   = note: expected struct `MyData<fn(&_)>`
              found struct `MyData<for<'a> fn(&'a _)>`

Uh, but this limitation makes T never be unsized though...

I think you can control unsizing if you're willing to move the indirection inward, like:

pub struct MyData<T: ?Sized> {
    id: TypeId,
    data: Box<T>,
}

Then the new_type you tried before will be rejected as a non-primitive cast.

You can write a helper method that coerces the inner Box and resets id, at least for specific types, but I don't think you can do that generically until Unsize is stable (or you use that on nightly).

impl<T: 'static> MyData<T> {
    pub fn unsize<U>(self) -> MyData<U>
    where
        T: Unsize<U>,
        U: ?Sized + 'static,
    {
        MyData {
            data: self.data, // coerced
            id: TypeId::of::<U>(),
        }
    }
}
3 Likes

Thanks, I think it is close to the best workaround. I think the rejection may be because the size of the pointer inside Box will grow when casting a sized type to unsized type (But I don't know why it can coerce when not surrounded by Box). But @quinedot 's subtyping example doesn't require size growth, so that unsound code can still pass the compilation.

let origin: MyData<fn(&str)> = MyData::new(|_| {});
let new_type: MyData<fn(&'static str)> = origin;
assert!(! new_type.type_id_eq());

Adding the phantom fn item I mentioned above will solve this problem.

pub struct MyData<T: ?Sized> {
    id: TypeId,
    _p: PhantomData<fn(T) -> T>,
    data: Box<T>,
}

As far as I know, the language offers no guaranted way to opt out of type-changing coercions (etc); if one you haven't thought of is or becomes possible in safe code, and unsoundness results, that would be on you.

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.