1

I have a trait Cell:

pub trait Cell: Debug + Clone + Ord {}

But when I try to create a vector of them like this:

pub struct Tuple {
    pub cells: Vec<Box<dyn Cell>>,
}

The compiler gave me the following error:

error[E0038]: the trait `storage::tuple::cell::Cell` cannot be made into an object
   --> src/storage/tuple/tuple.rs:20:24
    |
20  |     pub cells: Vec<Box<dyn Cell>>,
    |                        ^^^^^^^^ `storage::tuple::cell::Cell` cannot be made into an object
    |
note: 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>

So I was baffled after researching for a while:

  • What makes a trait "object safe", and what makes it not?
  • Can we inherit an "unsized" trait and make our child trait "size/object safe" at the same time?
  • What's the usage of a "not object safe" trait?
  • Why we cannot make a Box<dyn trait> when the trait is "not object safe"? Given the fact that we are manipulating objects on heap, why does the compiler still not let me go?

I tried:

  • Remove the "Ord" or "Clone" trait, but the trait doesn't make sense any more.
  • Add "+ Sized" to the trait, it doesn't fix the compile error.
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
Xiaochen Cui
  • 2,337
  • 2
  • 13
  • 13
  • https://doc.rust-lang.org/book/ch17-02-trait-objects.html – Peter Hall Mar 10 '23 at 02:46
  • A trait that is bounded by `Clone` cannot be object-safe because `Clone::clone()` returns `Self`. The same for `Ord`, because it has methods that have `Self` arguments. – Peter Hall Mar 10 '23 at 02:51
  • @PeterHall Thanks, I have repeatedly read that topic, but I’m still unable to fix the issue. Additionally, I’m curious as to why a trait with `Self` returning methods loses its object-self nature. If it’s not object-self, then how can we inherit it to our trait? – Xiaochen Cui Mar 10 '23 at 02:58

1 Answers1

2

What makes a trait "object safe", and what makes it not?

The exact rules as defined in the reference:

  1. All supertraits must also be object safe.
  2. Sized must not be a supertrait. In other words, it must not require Self: Sized.
  3. It must not have any associated constants.
  4. It must not have any associated types with generics.
  5. All associated functions must either be dispatchable from a trait object or be explicitly non-dispatchable:
    1. Dispatchable functions require:
      1. Not have any type parameters (although lifetime parameters are allowed),
      2. Be a method that does not use Self except in the type of the receiver.
      3. Have a receiver with one of the following types:
        • &Self (i.e. &self)
        • &mut Self (i.e &mut self)
        • Box<Self>
        • Rc<Self>
        • Arc<Self>
        • Pin<P> where P is one of the types above
      4. Does not have a where Self: Sized bound (receiver type of Self (i.e. self) implies this).
    2. Explicitly non-dispatchable functions require:
      • Have a where Self: Sized bound (receiver type of Self (i.e. self) implies this).

Generally, a trait is object-safe if we can create a vtable for it (not all rules are strictly necessary, but most of them are).

Here's a summary of the reasons for the rules:

  1. Obvious.
  2. Not strictly necessary, this is a way to explicitly opt-out for object safety. Sometimes it is wanted.
  3. Not strictly necessary, a question of syntax and need.
  4. It was just unusable: to make a dyn Trait for a trait with associated type, you need to specify the type of the associated type (dyn Trait<Assoc = Ty>). This is because the compiler may need to know the concrete type when the associated type is used, e.g. in methods. There is just no way to specify the type for a generic associated type, and the semantics are also somewhat unclear.
      1. As generic functions are monorphized, creating a new function for each instantiation, the vtable would need to contain entries for each possible instantiation, but there are infinitely such many.
      2. It cannot return Self, because to return a type we need to save space for it on the stack, but we don't know how big Self is because we don't know its concrete type. It cannot take Self as a parameter, because the semantics would be unclear: what type will the caller provide? If dyn Trait, it could provide a type different than Self, but the function is expecting the same type! If Self, we don't know what it is in the caller!
      3. The compiler needs to know the layout of the receiver. For normal methods, basically every type could be used as a receiver, and the question what to allow is a question of what we want, because the receiver is just a normal parameter with syntax sugar. But for methods dispatched via dyn Trait, The receiver needs to contain a vtable, so we know what method to use. Also, when we call the method, we call it with wide pointer (data pointer + vtable). But the method itself expects a thin pointer (only the data pointer), because it knows the concrete type. The compiler needs to be able to convert between the two. The current solution is just to use a fixed set of possible types, known to the compiler (more precisely, types that implement std::ops::DispatchFromDyn, and this trait has restrictions on what types can implement it). This may be changed in the future to allow the user to extend this list.
      4. Not necessary, just a way to opt-out for object safety for a specific method.
    1. A way to opt-out for object safety for a specific method, leaving the overall trait object-safe but with it being undispatchable via dyn Trait.

Can we inherit an "unsized" trait and make our child trait "size/object safe" at the same time?

If by "unsized" you mean "non object-safe", then the answer is no. As said above, one of the rules for object safety is that all supertraits are object safe.

What's the usage of a "not object safe" trait?

An object safe trait can be used for dynamic polymorphism (dyn Trait), in addition to static polymorphism (generics). Non-object-safe traits can only be used for static polymorphism. Most of our polymorphism in Rust is static, so many times this does not matter.

Why we cannot make a Box<dyn trait> when the trait is "not object safe"? Given the fact that we are manipulating objects on heap, why does the compiler still not let me go?

The very much definition of object safe trait is "allowed to be used in dyn Trait". If the trait is not object-safe, we cannot create the vtable part for Box<dyn Trait>.

So how do I fix that?

In general, you cannot. If a trait is not object-safe, it cannot be made object safe.

However, sometimes you can. For example, for Clone, see How to clone a struct storing a boxed trait object?. Ord is more complicated, but sometimes it is also possible, see How to test for equality between trait objects?.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • _"2. Not strictly necessary"_ -- Maybe that's _very strictly_ true. But if a trait requires that implementors are `Sized`, that implies that it needs to move an unboxed value somewhere. – Peter Hall Mar 10 '23 at 14:21
  • @PeterHall It could opt out for specific methods. I'm not saying it is not needed, just that it is not strictly necessary. – Chayim Friedman Mar 10 '23 at 14:28
  • Things would fall apart if `dyn Ord` doesn't implement `Ord`. Imagine a function with signature `fn foo(x: &T)`. If you have a variable `x: Box`, you wouldn't be able to call `foo(&*x)`, because `foo` _**might**_ call disallowed methods. To make that work, you'd have to have a way to name the object-safe super-trait of `Ord` let's say `$Ord` and defensively use `$Ord` in every constraint where you absolutely don't need to call `min`, `max` or `clamp`. – Peter Hall Mar 10 '23 at 14:56
  • @PeterHall I don't understand how you came to `Ord`, but anyway [this is still the case today and you just cannot call these methods in `foo()`](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d01377910818da9ca9d2e22e4ed00fcc). – Chayim Friedman Mar 10 '23 at 14:59
  • I picked Ord because it's one of OP's problem traits and it's only the "provided" methods that are problematic, so it was a plausible poster-child for your use-case. – Peter Hall Mar 10 '23 at 15:20
  • Your example might have convinced me, but I'm 100% sure yet. The fact that the method would require a `?Sized` bound might actually make this all coherent (together with the fact that I was mistaken about why `Ord` is not object-safe: it's because it has `Self` as type parameter, not because of the non-object-safe methods). – Peter Hall Mar 10 '23 at 15:33
  • @PeterHall No, `Ord` is not object safe because it has methods taking `&Self`. The fact that it has a type parameter defaulted to `Self` does not matter, only when the default is used. – Chayim Friedman Mar 11 '23 at 18:08