Why doesn't Rust support trait object upcasting?

kFYatek picture kFYatek · Feb 20, 2015 · Viewed 10.5k times · Source

Given this code:

trait Base {
    fn a(&self);
    fn b(&self);
    fn c(&self);
    fn d(&self);
}

trait Derived : Base {
    fn e(&self);
    fn f(&self);
    fn g(&self);
}

struct S;

impl Derived for S {
    fn e(&self) {}
    fn f(&self) {}
    fn g(&self) {}
}

impl Base for S {
    fn a(&self) {}
    fn b(&self) {}
    fn c(&self) {}
    fn d(&self) {}
}

Unfortunately, I cannot cast &Derived to &Base:

fn example(v: &Derived) {
    v as &Base;
}
error[E0605]: non-primitive cast: `&Derived` as `&Base`
  --> src/main.rs:30:5
   |
30 |     v as &Base;
   |     ^^^^^^^^^^
   |
   = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait

Why is that? The Derived vtable has to reference the Base methods in one way or another.


Inspecting the LLVM IR reveals the following:

@vtable4 = internal unnamed_addr constant {
    void (i8*)*,
    i64,
    i64,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*
} {
    void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
    i64 0,
    i64 1,
    void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
    void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
    void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
    void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}

@vtable26 = internal unnamed_addr constant {
    void (i8*)*,
    i64,
    i64,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*
} {
    void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
    i64 0,
    i64 1,
    void (%struct.S*)* @_ZN9S.Derived1e20h9992ddd0854253d1WaaE,
    void (%struct.S*)* @_ZN9S.Derived1f20h849d0c78b0615f092aaE,
    void (%struct.S*)* @_ZN9S.Derived1g20hae95d0f1a38ed23b8aaE,
    void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
    void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
    void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
    void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}

All Rust vtables contain a pointer to the destructor, size and alignment in the first fields, and the subtrait vtables don't duplicate them when referencing supertrait methods, nor use indirect reference to supertrait vtables. They just have copies of the method pointers verbatim and nothing else.

Given that design, it's easy to understand why this does not work. A new vtable would need to be constructed at runtime, which would likely reside on the stack, and that isn't exactly an elegant (or optimal) solution.

There are some workarounds, of course, like adding explicit upcast methods to the interface, but that requires quite a bit of boilerplate (or macro frenzy) to work properly.

Now, the question is - why isn't it implemented in some way that would enable trait object upcasting? Like, adding a pointer to the supertrait's vtable in the subtrait's vtable. For now, Rust's dynamic dispatch doesn't seem to satisfy the Liskov substitution principle, which is a very basic principle for object-oriented design.

Of course you can use static dispatch, which is indeed very elegant to use in Rust, but it easily leads to code bloat which is sometimes more important than computational performance - like on embedded systems, and Rust developers claim to support such use cases of the language. Also, in many cases you can successfully use a model which is not purely Object-Oriented, which seems to be encouraged by Rust's functional design. Still, Rust supports many of the useful OO patterns... so why not the LSP?

Does anyone know the rationale for such design?

Answer

kFYatek picture kFYatek · Feb 23, 2015

Actually, I think I got the reason. I found an elegant way to add upcasting support to any trait that desires it, and that way the programmer is able to choose whether to add that additional vtable entry to the trait, or prefer not to, which is a similar trade-off as in C++'s virtual vs. non-virtual methods: elegance and model correctness vs. performance.

The code can be implemented as follows:

trait Base: AsBase {
    // ...
}

trait AsBase {
    fn as_base(&self) -> &Base;
}

impl<T: Base> AsBase for T {
    fn as_base(&self) -> &Base {
        self
    }
}

One may add additional methods for casting a &mut pointer or a Box (that adds a requirement that T must be a 'static type), but this is a general idea. This allows for safe and simple (although not implicit) upcasting of every derived type without boilerplate for every derived type.