Why doesn't Rust support trait object upcasting?

OopRustLanguage DesignLiskov Substitution-Principle

Oop Problem Overview


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?

Oop Solutions


Solution 1 - Oop

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.

Solution 2 - Oop

As of Jun 2017, the status of this "sub-trait coercion" (or "super-trait coercion") is as follows:

  • An accepted RFC #0401 mentions this as a part of coercion. So this conversion should be done implicitly. > coerce_inner(T) = U where T is a sub-trait of U;
  • However, this is not yet implemented. There is a corresponding issue #18600.

There is also a duplicate issue #5665. Comments there explain what prevent this from being implemented.

  • Basically, the problem is how to derive vtables for super-traits. Current layout of vtables is as follows (in x86-64 case):
    +-----+-------------------------------+
    | 0- 7|pointer to "drop glue" function|
    +-----+-------------------------------+
    | 8-15|size of the data               |
    +-----+-------------------------------+
    |16-23|alignment of the data          |
    +-----+-------------------------------+
    |24-  |methods of Self and supertraits|
    +-----+-------------------------------+
    
    It doesn't contain a vtable for a super-trait as a subsequence. We have at least to have some tweaks with vtables.
  • Of course there are ways to mitigate this problem, but many with differing advantages/disadvantages! One has a benefit for the vtable size when there is a diamond inheritance. Another is supposed to be faster.

There @typelist says they prepared a draft RFC which looks well-organized, but they look like disappeared after that (Nov 2016).

Solution 3 - Oop

I ran into the same wall when I started with Rust. Now, when I think about traits, I have a different image in mind than when I think about classes.

trait X: Y {} means when you implement trait X for struct S you also need to implement trait Y for S.

Of course this means that a &X knows it also is a &Y, and therefore offers the appropriate functions. It would require some runtime-effort (more pointer dereferences) if you needed to traverse pointers to Y's vtable first.

Then again, the current design + additional pointers to other vtables probably wouldn't hurt much, and would allow easy casting to be implemented. So maybe we need both? This is something to be discussed on internals.rust-lang.org

Solution 4 - Oop

This feature is so desired that there is both a tracking issue for adding it to the language, and an dedicated initiative repository for the people contributing to implementing it.

Tracking Issue: https://github.com/rust-lang/rust/issues/65991

Initiative Repository: https://github.com/rust-lang/dyn-upcasting-coercion-initiative

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionkFYatekView Question on Stackoverflow
Solution 1 - OopkFYatekView Answer on Stackoverflow
Solution 2 - OopMasaki HaraView Answer on Stackoverflow
Solution 3 - Oopoli_obkView Answer on Stackoverflow
Solution 4 - OoppnkfelixView Answer on Stackoverflow