Overlapping implementations in std::error

I wonder how it's possible that there are multiple implementations of downcast for std::error::Error:

How does this work? Aren't these overlapping? Or are these distinct types? I guess they are distinct types then?

Indeed, they are distinct types. Says the reference:

Two trait object types alias each other if the base traits alias each other and if the sets of auto traits are the same and the lifetime bounds are the same. For example, dyn Trait + Send + UnwindSafe is the same as dyn Trait + UnwindSafe + Send .

There are no subtyping relationships between dyn A, dyn A + Send, dyn A + Send + Sync, etc. But you can "cast away" the auto traits:

let x: Box<dyn Any + Send + 'static> = Box::new(5) as _;
let y: Box<dyn Any + 'static> = x as _;

(Obviously the inverse cast is not allowed.)

2 Likes

I believe they are distinct types, yes, but can be converted into each other (you can pass a &dyn Display + Send into a function expecting &dyn Display)

2 Likes

I wonder why…

fn func(value: i32) {
    println!("{}", value)
}

fn execute(mut run: Box<dyn FnMut()>) {
    run()
}

fn main() {
    let i: i16 = 5;
    func(i as i32);
    let closure: Box<dyn FnMut() + Send + Sync> = Box::new(|| {
        println!("Hello")
    });
    execute(closure);
}

(Playground)

Output:

5
Hello

I can't remove the as i32 in the example above. That's not surprising. But I wonder why I can pass Box<dyn FnMut() + Send + Sync> to a function that expects Box<dyn FnMut()>, if both are separate types and there is no subtyping.

Don't get me wrong, I'm happy I can pass the closure here; it would be annoying if I couldn't. I'm just trying to find the corresponding rule or explanation in the reference. Maybe I've just been looking in the wrong place, or is this something that's not mentioned in the reference?

Integer type conversions are a cast that's not a coercion. Too many bugs would arise if these were silent coercions.

dyn A + B implements A, so dyn A + B: Unsize<dyn A> too, and you can coerce through the Box (even if the inner type on both sides are not Sized).


This also works without explicit casting:

fn func(value: Box<dyn Display>) {
    println!("{}", value)
}

fn main() {
    let b: Box<i16> = Box::new(5);
    func(b);
}

The main differences are that i16 is Sized, and it's common to have a mental barrier around thinking of dyn Whatever as a concrete type.

2 Likes

I hope I understand it right then that it's a combination of:

  • "Types implementing a trait Trait also implement Unsize<dyn Trait>" (see std::marker::Unsize),
  • impl<T, U, A> CoerceUnsized<Box<U, A>> for Box<T, A> where … (see std::ops::CoerceUnsized,
  • "[…], a type Foo<T> can implement CoerceUnsized<Foo<U>> when T implements Unsize<U> or CoerceUnsized<Foo<U>>." (see Unsized coercions in the reference).

Certainly should look at the unsized coercions more closely to understand them better.

I feel like there's something (fundamentally) new to learn about Rust everyday! Does this ever end? :sweat_smile:

That matches my understanding.

Not yet for me, and I do occasionally re-learn things that didn't truly stick as well...

1 Like

I just realized this only works with auto traits (like you said). I played around a bit:

#![feature(trait_upcasting)]

fn execute(run: Box<dyn FnOnce()>) {
    run()
}

trait Dropable {}
impl<T: ?Sized> Dropable for T {}

fn nop(value: Box<dyn Dropable>) {
    // we can't really do anything here but dropping the value
    println!("Before drop");
    drop(value);
    println!("After drop");
}

struct DropMonitor {}

impl Drop for DropMonitor {
    fn drop(&mut self) {
        println!("DropMonitor was dropped")
    }
}

fn main() {
    let closure1: Box<dyn FnOnce() + Send + Sync> = Box::new(|| {
        println!("Hello One");
    });
    execute(closure1);
    let closure2: Box<dyn Fn() + Send + Sync> = Box::new(move || {
        println!("Hello Two");

    });
    execute(closure2); // this requires #![feature(trait_upcasting)]
    let monitor = DropMonitor {};
    let closure3: Box<dyn FnOnce() + Send + Sync> = Box::new(move || {
        let _monitor = monitor;
        println!("Hello Three");

    });
    // This won't work at all:
    // nop(closure3);
    // Neither does this:
    // nop(closure3 as Box<dyn Dropable>);
    // But we can do:
    nop(Box::new(closure3));
}

(Playground)

Output:

Hello One
Hello Two
Before drop
DropMonitor was dropped
After drop

Note that we cannot cast or coerce Box<dyn FnOnce() + Send + Sync> into Box<dyn Dropable>. I assume that's because of the vtable being different, right?

Interestingly, it's possible to wrap it in another box to make it Dropable and thus passable to nop. (Side note: That trick can help me to be generic over the argument and return type of a closure in my ephemeral closure experiment.)

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.