Casting TraitObject to Super Trait

I want the following work:

trait Foo {
    fn method_foo(&self) -> String;
}

trait Bar : Foo
{
    fn method_bar(&self)->String;
}

impl  Foo for u8{
    fn method_foo(&self) -> String { format!("u8: {}", *self) }
}

impl Bar for u8{
    fn method_bar(&self) -> String { format!("string: {}", *self) }
}


fn main()
{
    let x= 5u8;
    let b:&Bar=&x as &Bar; //ok
    let f:&Foo=&x as &Foo; //ok
    let fb:&Foo=b as &Foo; //not ok
}

The compiler is throwing the following error:

error[E0605]: non-primitive cast: &dyn Bar as &dyn Foo

Is it possible to cast an trait object to an super trait?

Why do we need an explicit as cast when trait Bar is known to be a suptype of trait Foo?

You can't, see this thread for a work around

2 Likes

But wasn't this solution for downcasting a super trait object to an subtrait object, in my case it is the opposite direction or doesn't matter?

Initially, I thought that the vtable of a trait contains also the methods of the super traits such that a conversion to the super trait can also reuse this vtable, but I'm not sure if this is the case.

If the vtable of a trait does only contain the current trait method pointers it doesn't make sense at all to cast trait object to super traits.

True, but you can do the same thing, provide a method which converts to the super trait and put it in the sub trait.

They do contain the methods, but not in the same order (meaning they have a different in memory layout). So there is no way to perform the conversion automatically.

1 Like

They do contain the methods, but not in the same order (meaning they have a different in memory layout). So there is no way to perform the conversion automatically.

So, say trait T has methods m2, m3 an expect these methods in the same order in the vtable.
Super trait S has methods m1.
As I understand correctly, if a create a trait object O of type T, the methods m2,m3,m1 in the same order will be provided in the vtable of O which is not problem for Trait T, but for S as S expects m1 at position of m2.
So in the end, I need to remove the first two methods in the vtable in order to provide m1 at first or I put them at the end of the vtable.

How can I do that? Is this safe or is there some RTTI information available about the method signatures in the vtable of some trait object? What happened if I'm doing it wrong.

Thanks for your patience.

Trait objects have an unspecified layout, so you can't know the order that the methods show up in the vtable. If you try to rely on the layout, you will invoke UB.

The only way to do this conversion safely is to provide methods that convert to the super trait

You could even extract the conversion functions into their own trait like so.

I believe you're mistaken. In the example, there's no reason for dyn Bar to contain an entry for method_foo. Even if Bar were implemented so that it uses the methods of Foo, e.g.

impl<T: Foo> Bar for T {
    fn method_bar(&self) -> String { self.method_foo() }
}

Dispatching method_foo would be resolved at compile time and compiled into the monomorphized code for method_bar. The vtable for Bar does not contain any entries for methods not in Bar itself.

This compiles, note fn sub at the end

You can access super trait methods from a dynamically dispatched subtrait, meaning those functions from the super trait are in the vtable.

Now, a blanket impl is a different story, but that's also not what I was talking about.

3 Likes

I must have been confused. Thanks for the counterexample

Thanks @RustyYato,

this example clarified my understanding though I still don't understand why it is accepted that self of Type SubTrait{1,2} can be casted to SuperTrait, there is no evidence and if there is one, why I can't directly state: let superT:SuperTrait=subT //subT of type SubTrait{1,2}
That's really strange, is there any reason to forbid dyn trait to dyn trait casting between compatible types?
Is this worth to change it with a RFC as the workaround is frustrating?

Here the complete example:

pub trait SuperTrait1 {
    // .. methods ..
    
    fn super_method1(&self);
}

pub trait SuperTrait2 {
    // .. methods ..
    
    fn super_method2(&self);
}

pub trait SubTrait: SuperTrait1 + SuperTrait2 + IntoSuper<dyn SuperTrait1> + IntoSuper<dyn SuperTrait2> {
    // .. methods ..
    fn sub_method1(&self){println!("sub_method1");}
    fn sub_method2(&self){println!("sub_method2");}
    fn super_method1(&self){println!("SubTrait super_method1");}
    fn super_method2(&self){println!("SubTrait super_method2");}
}

impl SuperTrait1 for u8
{
    fn super_method1(&self){println!("super_method1");}
}

impl SuperTrait2 for u8
{
    fn super_method2(&self){println!("super_method2");}
}

impl SubTrait for u8
{
}



pub trait IntoSuper<Super: ?Sized> {
    fn as_super(&self) -> &Super;
    fn as_super_mut(&mut self) -> &mut Super;
    fn into_super(self: Box<Self>) -> Box<Super>;
}

impl<'a, T: 'a + SuperTrait1> IntoSuper<dyn SuperTrait1 + 'a> for T {
    fn as_super(&self) -> &(dyn SuperTrait1 + 'a) { self }
    fn as_super_mut(&mut self) -> &mut (dyn SuperTrait1 + 'a) { self }
    fn into_super(self: Box<Self>) -> Box<dyn SuperTrait1 + 'a> { self }
}

impl<'a, T: 'a + SuperTrait2> IntoSuper<dyn SuperTrait2 + 'a> for T {
    fn as_super(&self) -> &(dyn SuperTrait2 + 'a) { self }
    fn as_super_mut(&mut self) -> &mut (dyn SuperTrait2 + 'a) { self }
    fn into_super(self: Box<Self>) -> Box<dyn SuperTrait2 + 'a> { self }
}

fn foo1(s: &dyn SubTrait) -> &dyn SuperTrait1 {
    s.as_super()
}

//fn sub1(sub: &dyn SubTrait) {
//    SubTrait::super_method1(&sub1);
//}

fn foo2(s: &dyn SubTrait) -> &dyn SuperTrait2 {
    s.as_super()
}

//fn sub2(sub: &dyn SubTrait) {
//    SuperTrait::super_method2(&sub2);
//}

fn main()
{
    let x= 5u8;
    let subT:&dyn SubTrait = &x;
    let superT1:&dyn SuperTrait1 = &x;
    let superT2:&dyn SuperTrait2 = &x;
    //let superT:&dyn SuperTrait1  = subT as &dyn SuperTrait1;
    //subT.sub_method1();
    //subT.sub_method2();
    foo1(subT).super_method1();
    foo2(subT).super_method2();
}

When it runs it prints super_method1\nsuper_method2

1 Like

Does someone knows how I get sub1 and sub2 to run?

Whut, how does that not violate the Liskov Substitution principle? Is it because of the indirection introduced by the &, turning type Foo into &Foo (and similar reasoning for Box<Foo>, Arc<Foo>, Rc<Foo> etc)?

This is an OOP principle, and Rust doesn't support OOP well if at all. You can use the solution I posted above to perform the conversion, but there is no automatic way to have all of performance, low memory usage, dylib support, and automatic trait object upcasting. This is a hard problem that other languages bake into their runtime.

Rust doesn't support type level inheritance. However it does support something it calls trait inheritance and I'd expect that to adhere to the LSP (i.e. If some type T behaves like X, and X is a subtrait of Y, then T always behaves like Y as well).
The solid principles may originate in the OO world, but the principles are more general than that.

The LSP in particular says more about the type system and the substitutability of types than it does about the OO-ness of some piece of code.

Now, traits aren't types of course. However they do fulfill a somewhat similar role in that they name behavior rather than types. I'd expect trait inheritance to behave a lot like type inheritance precisely because of that, and because the issues generally present with type inheritance (eg the diamond problem for multiple inheritance) aren't present with trait inheritance because there's no possibility of conflicting impls.

If trait inheritance works drastically differently from type inheritance then it would be a really good move for the Rust Book to explain exactly how it differs, and why.

2 Likes

Given

struct Arg1;
struct Arg2;
struct Ret1;
struct Ret2;

trait SuperTrait {
    fn method_of_super_trait (self: &'_ Self, arg1: Arg1) -> Ret1;
}

trait SubTrait : SuperTrait {
    fn method_of_sub_trait (self: &'_ Self, arg2: Arg2) -> Ret2;
}

Then the layout of "<dyn SubTrait>::VTable" would be something like (note: this is not guaranteed at all by the specs, but it helps understand how this works).

#[repr(C)]
struct DynSubTraitVTable {
    size: usize,
    align: usize,
    drop_in_place: unsafe fn (this: *mut ()),
    method_of_super_trait: unsafe fn (this: *const (), arg1: Arg1) -> Ret1,
    method_of_sub_trait: unsafe fn (this: *const (), arg2: Arg2) -> Ret2,
}

and is initialized with

impl DynSubTraitVTable {
    const
    fn of<T : SubTrait> () -> Self
    {
        Self {
            size:
                ::core::mem::size_of::<T>()
            ,
            align:
                ::core::mem::align_of::<T>()
            ,
            drop_in_place: erase_input_type!(
                ::core::ptr::drop_in_place::<T>
            ),
            method_of_super_trait: erase_input_type!(
                <T as SuperTrait>::method_of_super_trait
            ),
            method_of_sub_trait: erase_input_type!(
                <T as SubTrait>::method_of_sub_trait
            ),
        }
    }
}

/// And for the SuperTrait
impl DynSuperTraitVTable {
    const
    fn of<T : SuperTrait> () -> Self
    {
        Self {
            size:
                ::core::mem::size_of::<T>()
            ,
            align:
                ::core::mem::align_of::<T>()
            ,
            drop_in_place: erase_input_type!(
                ::core::ptr::drop_in_place::<T>
            ),
            method_of_super_trait: erase_input_type!(
                <T as SuperTrait>::method_of_super_trait
            ),
        }
    }
}

So granted, each vtable has its own function pointer, but by design the lookup always leads to the the address of the compile-time / static method for that.
That is, for any obj: T where T : SubTrait, then both

  • (&obj as &dyn SubTrait).vtable!().method_of_super_trait

  • (&obj as &dyn SuperTrait).vtable!().method_of_super_trait

point to the same static function: <T as SuperTrait>::method_of_super_trait

What if trait SubTrait contains also a method_of_super_trait method, after type erasure you get a collision.
I always thought it worked by order, the submethods in the vtable are below the super methods, i.e. each trait expects a method to exists at some specific offset.

Yes yes, since for the compiler each method is uniquely identified by the trait it belongs to, there is no "name clash" contrary to what my presentation may suggest; that point was indeed not clear nor clarified, so thanks for bringing that up.

What I wanted to say is that a vtable contains a succession of function pointers (and using a struct with named fields, for my explanation, made that easier to follow than a fixed-size-array with hard-coded indices).

That being said, know that if a trait and a subtrait each a method with the same name, then, when given an instance of something that implements the subtrait, .method() notation cannot be sued as that would be ambiguous; so one needs to qualify the name of the method by the name of the trait it belongs to:

trait Super { fn method (&self); }
trait Sub : Super { fn method (&self); }

fn foo (x: impl Sub)
{
    x.method(); // Error: ambiguous
    Sub::method(&x); // OK
    Super::method(&x); // OK
}

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.