80

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
kFYatek
  • 5,503
  • 4
  • 21
  • 14
  • 20
    As a side note: Rust is not object-oriented language. Traits are not interfaces, they are more like type classes from Haskell. Rust also does not have subtyping, so LSP is somewhat inapplicable to it because its definition is tied to the subtyping relationship. – Vladimir Matveev Feb 20 '15 at 16:23
  • 13
    Still, as I said, Rust supports a lot of OO-style abstractions, and traits are allowed to inherit, forming something akin to a type hierarchy. For me, it would seem natural to support LSP for trait objects, even if OO isn't the main paradigm of the language. – kFYatek Feb 20 '15 at 16:37
  • 1
    Please make sure to upvote useful answers and mark an answer as accepted if it solved your problem! If no answer is acceptable, consider leaving comments explaining why, or edit your question to phrase the problem differently. – Shepmaster Mar 03 '15 at 01:22
  • 1
    There's a Rust issue for this question: https://github.com/rust-lang/rust/issues/5665 (which I see you've already found; just placing a link to it here.) – Jim Blandy Mar 06 '15 at 17:07
  • how did you get this LLVM IR? – Guerlando OCs Jul 08 '21 at 06:57

5 Answers5

71

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
kFYatek
  • 5,503
  • 4
  • 21
  • 14
  • Unfortunately the usage area of this `impl Trait` approach is somewhat narrow, if a complex trait architecture is used. http://play.integer32.com/?gist=bbe93906ddab1beaa34eb33e11eda41a&version=nightly Without the possibility of elimination of the intersecting implementations (i.e. `impl `) it is required to install the `as_*()` methods directly into the nesting trait to prevent conflicts. – snuk182 Jan 16 '18 at 09:43
  • @Shepmaster Should this be `AsRef` in today's Rust? – Bergi Jul 10 '18 at 13:12
  • 1
    @Bergi `AsRef` has been available since Rust 1.0, but I'm not sure if you can use it here or not. Some [quick attempts](https://play.rust-lang.org/?gist=286334f687256c767cee13088f93f305&version=stable&mode=debug&edition=2015) show various errors. – Shepmaster Jul 11 '18 at 02:24
  • 2
    "One may add additional methods for casting a &mut pointer or a Box" — could you provide an example for the Box case? It's not clear to me how you make it work in that case without using unsafe. – Chris Suter Mar 14 '20 at 02:59
  • 1
    @kFYatek I have a similar approach in my codebase, but I struggle to get it working with `Box`. Have you been successfully doing it already? – phimuemue Mar 28 '20 at 17:20
  • The crate `downcast-rs` implements the above pattern for upcasting to Any, which then allows for downcasting to a concrete type. It also supports `Box`. https://crates.io/crates/downcast-rs – sffc May 06 '20 at 07:06
  • This will only work if you have a pair of Traits, one inheriting from another. It doesn't generalize to more complex hierarchies, unless you add a "as_X" for each "X" you want to upcast to. – FreelanceConsultant Jun 04 '23 at 19:00
29

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).

Masaki Hara
  • 3,295
  • 21
  • 21
  • This now works on nightly (under `#![feature(trait_upcasting)]`) and even a candidate for stabilization! But there were some concerns about executable sizes. – Chayim Friedman May 10 '23 at 22:26
23

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

Earth Engine
  • 10,048
  • 5
  • 48
  • 78
oli_obk
  • 28,729
  • 6
  • 82
  • 98
  • can you develop ? Particularly how to handle or workaround with a different desing ? I exactly run into the same wall after my first two hours of rust coding :/ – sandwood Jan 14 '22 at 22:06
2

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

pnkfelix
  • 3,770
  • 29
  • 45
1

This is now working on stable rust, you can upcast to the base trait also you can call base trait functions directly from the derived trait object

trait Base {
   fn a(&self) {
     println!("a from base");
   }
}

trait Derived: Base {
   fn e(&self) {
     println!("e from derived");
   }
}

fn call_derived(d: &impl Derived) {
   d.e();
   d.a();
   call_base(d);
}

fn call_base(b: &impl Base) {
   b.a();
}

struct S;
impl Base for S {}
impl Derived for S {}

fn main() {
   let s = S;
   call_derived(&s);
}

playground link

Mohammed Essehemy
  • 2,006
  • 1
  • 16
  • 20