Have supertrait upcasting coercions finally landed in stable?

I remember this being a limitation in Rust for years... but it works now??

trait Base {
    fn b(&self);
}

trait Derived: Base {
    fn d(&self);
}

struct S;

impl Base for S {
    fn b(&self) {
        println!("b");
    }
}

impl Derived for S {
    fn d(&self) {
        println!("d");
    }
}

fn main() {
    let d: &dyn Derived = &S;
    d.b(); // compiles now without an explicit upcast to &dyn Base???
    d.d();
}

playground

Is there an RFC or release notes somewhere where I can read more about this change?

This has not changed. After converting it to use 2015 syntax (change &dyn Derived to &Derived), this code compiles successfully under rustc 1.0.0.

All types that implement Derived must also implement Base. This means that the dyn Derived type, which auto-implements Derived, must also auto-implement Base.

However, this does not mean you can cast it to &dyn Base. The value passed to the function still has type &dyn Derived. Upcasting is still not supported:

    // DOES NOT COMPILE
    let d: &dyn Derived = &S;
    let b: &dyn Base = d; // error[E0308]: mismatched types

As a side note, if you implement Base for references, you can use double indirection to cast &&dyn Derived to &dyn Base:

// ...

impl<T: Base + ?Sized> Base for &T {
    fn b(&self) {
        (*self).b()
    }
}

fn main() {
    let d: &dyn Derived = &S;
    let b: &dyn Base = &d;
}
1 Like

The double indirection trick is pretty neat.

However, what's the point in upcasting to dyn Base if I can already call all of Base's methods on a dyn Derived though? Is it to satisfy the type-checker when I have a function that only takes a dyn Base arg?

trait Base {
    fn b(&self);
}

trait Derived: Base {
    fn d(&self);
}

struct S;

impl Base for S {
    fn b(&self) {
        println!("b");
    }

}

impl Derived for S {
    fn d(&self) {
        println!("d");
    }
}

fn only_takes_base(b: &dyn Base) {
    b.b();
}

fn main() {
    let d: &dyn Derived = &S;
    only_takes_base(d); // compile error
}

playground

Are there any other use-cases where I may have a dyn Derived but I can only use a dyn Base?

You may have a collection somewhere that's holding dyn Bases that you want to store your dyn Derived in.

Onother way to do such upcasting involves adding a method

trait Base {
    fn b(&self);
    fn as_dyn_base(&self) -> &dyn Base;
}

// #![feature(specialization)] makes this nicer with:
/*
default impl<T> Base for T {
    fn as_dyn_base(&self) -> &dyn Base {self}
}
*/

trait Derived: Base {
    fn d(&self);
}

struct S;

impl Base for S {
    fn b(&self) {
        println!("b");
    }
    
    // not needed with specialization and the default impl above
    fn as_dyn_base(&self) -> &dyn Base {self}

}

impl Derived for S {
    fn d(&self) {
        println!("d");
    }
}

fn main() {
    let d: &dyn Derived = &S;
    d.b(); // compiles now!?
    let d1: &dyn Base = d.as_dyn_base();
    d1.b();
    d.d();
}

Another follow-up question, if this works:

fn main() {
    let d: &dyn Derived = &S; // &(dyn Derived + Base)
}

then why does this not work:

trait Trait {
    fn print(&self) {
        println!("trait print");
    }
}
trait Trait2 {
    fn print2(&self) {
        println!("trait2 print");
    }
}

struct Struct;

impl Trait for Struct {}
impl Trait2 for Struct {}

fn main() {
    let t: &(dyn Trait + Trait2) = &Struct; // compile error
}

The &dyn Derived above is basically the same as a &(dyn Derived + Base) so why can I make some &(dyn Trait + Trait2) where there's no relationship between Trait and Trait2?

There is no (dyn Derived + Base) here. There is no such type in Rust.

The type is dyn Derived, period. This type does implement the Base trait, just like any other type can implement the Base trait. However, the reference stored in d has a pointer to a Derived vtable, not a Base vtable. So you can pass it to a function that expects impl Base or impl Derived or dyn Derived, but not to a function that expects dyn Base.

The reason is that trait object pointers are currently always (data, vtable) fat pointers, and a vtable is always for a single trait. Allowing more complex cases would require different layouts of fat pointers and/or vtables.

1 Like

Oh okay, so dyn Derived implements Derived and Base so its vtable has function pointers for both traits' methods. I guess my last question is if the Rust compiler auto-implements Derived and Base for dyn Derived why doesn't it do the same for dyn Trait + Trait2 ? The workaround is trivial, as far as I know:

trait Trait {
    fn print(&self) {
        println!("trait print");
    }
}
trait Trait2 {
    fn print2(&self) {
        println!("trait2 print");
    }
}

// empty trait to combine Trait & Trait2
trait Trait3: Trait + Trait2 {}

// auto-impl Trait3 for any type that also implements Trait + Trait2
impl<T: Trait + Trait2> Trait3 for T {}

struct Struct;

impl Trait for Struct {}
impl Trait2 for Struct {}

fn main() {
    let t: &dyn Trait3 = &Struct; // &dyn Trait3 instead of &(dyn Trait + Trait2) 
    t.print(); // prints "trait print"
    t.print2(); // prints "trait2 print"
}

Seems like something the compiler should be able to easily automate, but I'm probably missing something, so why doesn't it?

I believe it's mostly just because the work to create and vet a detailed design for multi-trait objects, upcasting, etc. has mostly not been done yet. There have been discussions about it off-and-on, but I don't think it's been a high priority so far:

Which is understandable from an FTE POV, but also weird given the many :+1:t3: and :heart: on the issue.

But again, the workaround is simple: just make a new trait that combines the two and is automatically implemented.

The simplicity and obviousness of the explicit workaround solution probably also serves as reasoning against prioritizing synthesized multiple trait objects over other work.

One example of a difficult question that tangles multiple trait objects with upcasting: are dyn (A + B) and dyn (B + A) compatible? If upcasting is only allowed to the first of the two, then they aren't, but especially if upcasting isn't used/allowed, this seems like an unnecessary and unnatural restriction.

1 Like