Understanding Traits and Object Safety
Asked Answered
R

2

10

I am struggling with the basics of object safety. If I have this code

struct S {
    x: i32,
}

trait Trait: Sized {
    fn f(&self) -> i32
    where
        Self: Sized;
}

fn object_safety_dynamic(x: Trait) {}

I receive

error[E0038]: the trait `Trait` cannot be made into an object
  --> src/lib.rs:11:29
   |
5  | trait Trait: Sized {
   |       -----  ----- ...because it requires `Self: Sized`
   |       |
   |       this trait cannot be made into an object...
...
11 | fn object_safety_dynamic(x: Trait) {}
   |                             ^^^^^ the trait `Trait` cannot be made into an object

When I add or remove : Sized as the supertrait or as f's bound, I receive slightly different error messages.

Could someone explain:

  • Why does this particular example not work? The chapter Trait Objects states:

    So what makes a method object-safe? Each method must require that Self: Sized

    Isn't that fulfilled?

  • What is the difference between Trait: Sized and where Self: Sized? (Well, yes, one inherits the trait and the other one is a parameter bound, but from Rust's trait object perspective?

  • What is the preferred change I had to make object_safety_dynamic work?

I am using rustc 1.19.0-nightly (01951a61a 2017-05-20) if it matters.

Addressing the comment on fixed sizes.

trait TraitB {
    fn f(&self) -> i32
    where
        Self: Sized;

    fn g<T>(&self, t: T) -> i32
    where
        Self: Sized;
}
Ry answered 21/5, 2017 at 11:18 Comment(1)
Note that the section on trait object-safety was removed from the book. Details can be found in the Rust reference.Marieann
O
23

Why does this particular example not work? The chapter Trait Objects states:

So what makes a method object-safe? Each method must require that Self: Sized

Isn't that fulfilled?

This question really is: What is a trait object?

A trait object is an interface in the Object-Oriented paradigm:

  • it exposes a limited set of methods,
  • which are applied to an unknown concrete type.

The fact that the concrete type to which the operations is applied is unknown is specifically why one uses a trait object, as it allows manipulating a heterogeneous set of types in a uniform fashion down to the assembly level.

The fact the concrete type is unknown, however, means that the size of the memory area which contains the memory is also unknown; therefore a trait object can only be manipulated behind a reference or pointer such as &dyn TraitObject, &mut dyn TraitObject or Box<dyn TraitObject> for example.

At the memory level, each of them is represented identically:

  • a pointer to a virtual table, which is a structure holding one function pointer per "method" of the trait object at a fixed offset,
  • a pointer to the actual data of the object.

What is the difference between Trait: Sized and where Self: Sized? (Well, yes, one inherits the trait the other one is a parameter bound, but from Rust's trait object perspective?)

There is no inheritance in Rust. In both cases those are bounds:

  • Trait: Sized states that the trait itself can only be implemented for a type that already implements Sized,
  • fn method(&self) where Self: Sized states that only types that implement Sized can implement this method.

Note: when implementing a trait, all methods must end up having a definition; the latter is therefore only really useful if a default implementation is provided for the method with the Self: Sized bound, as is shown here.

What is the preferred change I had to make object_safety_dynamic work?

You have to take the trait object by reference or pointer. Whether you use a reference or pointer depends on whether you want to transfer ownership or not.

Orin answered 21/5, 2017 at 13:19 Comment(4)
Thanks! I get 1 and 3, but about 2: a) If I have a TraitB (see above), implemented by 5 types, used throughout my code with .g<T>() where T are 5 other types, the compiler would generate 5*5=25 implementations of g, since it could not statically determine which one is actually needed? (if so, how would that work from libraries where the callers are not known?) And b) why does adding where Self: Sized; change any of this, since it apparently only applies to the type implementing it, not TRy
I just realized where Self: Sized actually precludes it's use from within object_safety_dynamic ... I think I fundamentally misunderstand where.Ry
@left4bread: Regarding (a), it doesn't work. There can be a single implementation of g for each impl TraitB block if it is to be used as a trait object. Thus the restriction that trait objects methods NOT be generic over types (they can be generic over lifetimes).Orin
@trentcl: Indeed, fixed :)Orin
D
6

Making Trait a supertype of Sized doesn't help - in fact it is not permitted, as the error message says. Each implementation of Trait will still have a different size, so your function object_safety_dynamic cannot be compiled. Monomorphization cannot be used here because there is no generic parameter, so the compiled function must work for all implementations of Trait.

However, references do have a fixed size, so making the argument into a reference will work:

trait Trait {
    fn f(&self) -> i32;
}

fn object_safety_dynamic(x: &Trait) {}

A trait object is always a reference of some kind, e.g. a Box<T> or &T. This is precisely because the size of implementations of the trait will be different, while a reference type has a known, fixed size.

Dougie answered 21/5, 2017 at 11:31 Comment(4)
Isn't monomorphization what would be used for static dispatch? He/she is trying to do dynamic dispatching so that should not matter. In fact with monomorphization it would work as different functions are created that can take different sizes. Your main point is still correct though. Sorry if this is wrong ;)Towny
@Towny You are not wrong. He isn't trying to monomorphize, but he also wrote x: Trait as the argument. Dynamic dispatch must use references of some kind.Dougie
Thanks, but I have the same question as @Towny ... Also, with the implication that I apparently can't move traits but only use borrows / pointers, why do there need to be the Sized restrictions anyways? Couldn't the compiler handle this for arbitrary types?Ry
@left4bread: Not really. If you had x: Trait, the compiler cannot know how big x is because it could be implemented by types of any size. It can't know how much stack space to reserve. It doesn't know the size of struct fields. It doesn't know how big an element of a Vec should be. The only way it could make that work in general would be to put it on the heap behind a pointer... and now we're back to square one again.Freefloating

© 2022 - 2024 — McMap. All rights reserved.