5

Rust doesn't allow me to write code like this:

trait MyTrait {
    fn foo();
}

impl MyTrait for u8 {
    fn foo() {}
}

fn main() {
    let v = 1u8;
    let o = &v as &dyn MyTrait;
}

Adding where Self: Sized to fn foo() as the compiler suggests makes it work:

trait MyTrait {
    fn foo() where Self: Sized;
}

My first question is, why do I need a constraint for something that I'm not using?

Furthermore, if I understand it correctly, adding where Self: Sized makes a non-dispatchable function, which means I can't call o.foo(). (This doesn't quite apply to a non-method function, but just to emphasize its unappealing side effect.) I would need other solutions to make a dispatchable function. Another solution is to add &self as the first parameter, turning the function into a method, but I can't find an intuitive explanation why a method works here but a function doesn't. This raises a similar question: Why must I add a parameter that I'm not using (just to make the code compile)?

Another example that is not working is:

trait MyTrait {
    fn foo(&self) -> &Self;
}

impl MyTrait for u8 {
    fn foo(&self) -> &Self {
        self
    }
}

fn main() {
    let v = 1u8;
    let o = &v as &dyn MyTrait;
}

My second question is, why can't we return &Self (note: not Self) from a trait object?

I'm not seeking possible fixes (compiler has them). Instead I'm asking for the reasons behind these design decisions.

Boann
  • 48,794
  • 16
  • 117
  • 146
TSK
  • 509
  • 1
  • 9
  • I don't think it covers everything, but a lot of the explanation you're looking for seems to be covered under [Why can a function on a trait object not be called when bounded with `Self: Sized`?](https://stackoverflow.com/q/51822118/364696) (I think it indirectly answers your first question, and also answers directly the follow-up to the first question "Why must I add a parameter that I'm not using (just to make the code compile)?", it's only the second question isn't answered) – ShadowRanger Oct 26 '22 at 01:27
  • @ShadowRanger No, I got nothing from reading that answer. – TSK Oct 26 '22 at 01:59
  • Please ask only one question per SO question. – Chayim Friedman Oct 26 '22 at 06:48
  • @ChayimFriedman They are highly related? – TSK Oct 26 '22 at 06:57
  • I recommend you to change the title of your question. You don't ask why your trait is not object-safe. It's very trivial. However, what you ask is "Why trait must be object-safe to build trait object" – HyunWoo Lee Oct 26 '22 at 07:16

1 Answers1

2

I try to explain it without considering very deep details of rust compiler. So warning, this answer may not be 100% correct, but you can get at least some intuition of what's going on.

Step1 : Let's check compiler's advice.

for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit https://doc.rust-lang.org/reference/items/traits.html#object-safety

Simply speaking, if you want to make trait object, such trait must be object safe. To make trait object safe, there are several conditions but focus on this sentence at first.

All associated functions must either be dispatchable from a trait object or be explicitly non-dispatchable:

Step2 : what "dispatchable" means?

Consider following code.

    fn bar<T:MyTrait>(_ : T ){
        T::foo();
    }

This works perfectly fine. However, let's imagine that you try to do similar thing with trait object.

fn bar_dyn(v : &dyn MyTrait ){
   //...
}

Hmm... ok. How can we call foo? There is no way to call foo in this bar_dyn. But you may want to use foo because you implemented it. It's very akward situation. So rust compiler try to prevent this. That's why associated function of object safe trait must be "dispatchable". You can check if foo satisfy every requirement to be dispatchable, you now can call fooin above example.

Step3 : Why adding where Self: Sized resolve the problem?

But in some cases, you still want to use trait object even though you cannot use function foo. So, there's trick. You can tell compiler like this. "Hey. I know that I cannot use function foo via trait object and it's intentional."

By adding where Self: Sized, you restrict that the type or object which call foo must have known size at compilation time. However, it's clear that size of trait object cannot be known at compilation time because basically any type can implement MyTrait. Now look at the below.

    trait MyTrait {
        fn foo() where Self:Sized;
        fn goo(&self);
    }
    
    impl MyTrait for u8{
        fn foo() {}
        fn goo(&self) {}
    }

    fn bar_dyn(v : &dyn MyTrait ){
        v.goo();
    }

It works! Because by adding where Self: Sized, you tell the compiler that you promise to do nothing about foo via trait object.

Step4: Why can't we return &Self from a trait object?

If we allow it, very dangeruos thing happens.

    trait MyTrait {
        fn foo(&self)->&Self;
    }
    
    impl MyTrait for u8 {
        fn foo(&self)->&Self{self}
    }

    impl MyTrait for String {
        fn foo(&self)->&Self{self}
    }
    

    fn baz<'s, T:MyTrait+?Sized>( a: &'s T,b: &'s T)->(&'s T,&'s T){
        (a.foo(),b.foo())
    }

    fn main() {
        let v1 = 1u8;
        let v2 = String::from("hello");
        let o1 = &v1 as &dyn MyTrait;
        let o2 = &v2 as &dyn MyTrait;

        let (r1,r2) = baz(o1,o2);
    }

This code is not working because we cannot make trait obejct of MyTrait. However, assume we can, and let's see what happens. r1 and r2 is ensured to have same type by baz. And o1 and o2 have same type which implement trait MyTrait. (Every trait object implements correspodning trait) So we can call baz(o1,o2). And what? Now r1 has type u8 and r2 has type String. It yields huge error of rust type checker.

HyunWoo Lee
  • 125
  • 5
  • Is `where Self: Sized` a good criteria for dispatchability? What if the type implementing the trait has unknown size too? OTOH, in your Step4, the compiler can still report an error if it cannot verify the return value of `baz` has two items of the same type? – TSK Oct 26 '22 at 07:46