Why can't some traits be made into objects

1.5k views Asked by At

I understand the rules for when a trait can be made into a trait object, but I don't understand why these rules exist.

For example:

trait Resource {
    const RESOURCE_ID: u64;
}

trait ResourceStatic {
    fn static_id() -> u64;
}

trait ResourceInstance {
    fn resource_id(&self) -> u64;
}

struct MyResource {}

impl Resource for MyResource {
    const RESOURCE_ID: u64 = 123;
}

impl ResourceStatic for MyResource {
    fn static_id() -> u64 {
        123
    }
}

impl ResourceInstance for MyResource {
    fn resource_id(&self) -> u64 {
        123
    }
}

It seems to me all three traits are basically encapsulating the same functionality. Why is it then that these are not allowed:

let _: Box<dyn Resource> = Box::new(MyResource{});
let _: Box<dyn ResourceStatic> = Box::new(MyResource{});

but this is?

let _: Box<dyn ResourceInstance> = Box::new(MyResource{});

Can someone please explain what is going on under the hood so that it doesn't seem arbitrary?

Rust playground

1

There are 1 answers

0
trent On BEST ANSWER

What is a trait object? It is

  • a value,
  • of a concrete type not known to the compiler,
  • which nevertheless implements a trait.

This definition is sufficient to explain why ResourceInstance works while Resource and ResourceStatic don't.

ResourceInstance

trait ResourceInstance {
    fn resource_id(&self) -> u64;
}

This trait can be made into an object because even when the concrete type is not known, you can still call resource_id on a value that implements the trait (by passing it as the self parameter).

ResourceStatic

trait ResourceStatic {
    fn static_id() -> u64;
}

This trait cannot be made into an object, because static_id can be called without a value, which means in order to call static_id you must know the concrete type.

For each trait object type (e.g. dyn ResourceStatic), the compiler automatically generates an implementation of the corresponding trait (ResourceStatic). This automatic implementation uses the vtable pointer passed as part of the self type in the trait methods. When there is no self type there is no vtable pointer and the compiler can't automatically implement that method. There are no "bare vtable pointers" in Rust.

To perhaps understand this better, imagine dyn ResourceStatic is a valid type. What does <dyn ResourceStatic>::static_id() do? It cannot defer to the implementation of the concrete type, because there is no value and therefore no concrete type. Shall we conclude that dyn ResourceStatic doesn't implement ResourceStatic? That seems obviously wrong. Or does dyn ResourceStatic have its own implementation of ResourceStatic that doesn't defer to some concrete type? That also doesn't make sense, because the whole point of dyn ResourceStatic is to stand in for a concrete type.

The way Rust resolves this problem is simply to reject dyn ResourceStatic as a type.

Resource

trait Resource {
    const RESOURCE_ID: u64;
}

This trait cannot be made into an object for the same reason ResourceStatic cannot: because it's impossible for the trait object type dyn Resource to automatically satisfy the requirements of the trait.

TL;DR

If you want dynamic dispatch on the type Self, you need a self argument to dispatch on.