Can't use trait in option as trait object

Looks like I can't use an option with a boxed value as a trait object, any ideas why? Is there a way to get around this?

trait Trait {}

struct Struct {
    pub field: bool,
}

impl Trait for Struct {}

fn main() {
    let val = Some(Box::new(Struct {field: true}));

    if let Some(val) = val {
        println!("{}", val.field);
    }

    func(val);
}

fn func(__: Option<Box<dyn Trait>>) {

}

Getting error: expected trait object 'dyn Trait', found struct 'Struct'

Your Box can only get implicitly casted to a Box<dyn Trait> if it's a single layer. If it's nested (such as within an Option), you need to explicitly cast it beforehand with as. Try:

let val = Some(Box::new(Struct {field: true}) as Box<_>);

To let the inference engine know that you don't want an Option<Box<Struct>>, and instead something else.

Problem is that I do want a Option<Box<Struct>> first, but I then wanna pass it into a function that takes a Option<Box<dyn Trati>>

Then you can add an opportunity to convert it after the first step:

// use val as Option<Box<Struct>>

let val = val.map(|s| s as Box<dyn Trait>);

// use val as Option<Box<dyn Trait>>`
4 Likes

You can use Option::map, e.g. func(val.map(|x| x as _))

Rust Playground

kpreid yeah, I guess I can do that with option, but I have a similar situation with another type (Rc, don't think there's a way to .map an Rc like that).

Why would the compiler make me jump around like this? If it would be a Box<Struct> struct it would have been fine, why is that not the case for any other type holding a Box<Struct> like Option and Rc?!

Because a Box of dyn is a different data structure than a Box of a regular type — it has an extra pointer. There has to be specific support for the conversion.

You can see the list of types that you can coerce this way at the trait impls for CoerceUnsized. Notice they're all pointer and cell types — not other kinds of containers.

2 Likes

Type coercions are inserted at the same time as the compiler does type checking. Basically, the compiler inserts a coercion when it sees a value of known type X being stored in a place (e.g. a local, a temporary, a call argument, a return value) of a known type Y, where it can see that X and Y are different.

The coercion of Box<Struct> to Box<dyn Trait> is simple but not trivial; It requires adding a vtable. If you had some arbitrary generic struct, then GenericStruct<Box<Struct>> and GenericStruct<Box<dyn Trait>> could potentially have totally different layouts, so the compiler can't easily insert coercions for arbitrary nested types like this. Instead, you must coerce the Box itself before placing it inside.

Without something like as _ to introduce another unknown, the compiler will decide that the type of the box is Box<Struct> before it finishes checking the rest of the function, and the opportunity for inserting a coercion is lost. This is just how the type checker works, and it's what allows you to normally call methods of Struct on Box::new(the_struct) without having to declare it explicitly as Box<Struct>.

2 Likes

To formulate the "coercions are inserted at type-check time" part differently: Box<dyn Trait> is not a supertype of Box<Struct implementing Trait>. It's a concrete type (for each trait) that happens to implement the trait by forwarding to the implementation to another concrete type. It is not a superclass, in particular, although the dynamic dispatch mechanism is necessarily similar to how inheritance is implemented via vtables.

To draw a more extreme analogue: you can represent an integral number either as e.g. u64 or as a String containing the decimal expansion of the number. Both can hold the same information and you can use them for the same purpose (although doing so is neither elegant nor practical). There is a conversion between them (you can to_string and integer and parse() a string), but this doesn't make either one a subtype or supertype of the other.

5 Likes

Thanks all for the clear explanation!

Hope you're all okay with me asking a follow up.

I planned on having two structs existing simultaneously with an Rc to a trait object, one with referring to it with it's type and one referring to it as a trait object, from your answers I understand that this is not going to be possible since they might have different memory layouts, is that true? If so, is there any other way I can have both of them share an Rc to the same trait object?

use std::rc::Rc;

struct Shared;

trait Trait {}

impl Trait for Shared {}

struct StrictParent {
    child: Rc<Box<Shared>>,
}

struct LenientParent {
    child: Rc<Box<dyn Trait>>,
}

fn main() {
    let child = Rc::new(Box::new(Shared));

    let strict_parent = StrictParent {
        child: Rc::clone(&child)
    };

    let lenient_parent = LenientParent {
        child
    };
}

You could just use Rc<Shared> and Rc<dyn Trait>.

Rust Playground

2 Likes

Rc is already a pointer, you don't need to (and shouldn't) put a box inside it.

:person_facepalming: Really sorry! Used Box instead of Option.

Option<dyn Trait> is not a well-formed type though. You could use Option<Rc<dyn Trait>> (and Option<Rc<Shared>> if that fits your use-case. Or you could change Trait’s API and the impl Trait for Shared in a way that Trait is implemented by Option<Shared> instead of Shared, and then use Rc<Option<Shared>> and Rc<dyn Trait>. [Or use an additional trait; and with a blanket impl based on Trait… it all depends on what kinds of operations you actually require.]

2 Likes

Sorry again! Now I remember, this is why I had the Box in there.

It's meant to be Rc<Option<Box<dyn Trait>>>

Hope I'm not driving you crazy with my mistakes here...

FYI, here’s one possible example of what I meant by that:

use std::rc::Rc;

struct Shared;

trait Trait {}

trait OptionTrait {
    fn get(&self) -> Option<&dyn Trait>;
}

impl<T: Trait> OptionTrait for Option<T> {
    fn get(&self) -> Option<&dyn Trait> {
        self.as_ref().map(|r| r as _)
    }
}

impl Trait for Shared {}

struct StrictParent {
    child: Rc<Option<Shared>>,
}

struct LenientParent {
    child: Rc<dyn OptionTrait>,
}

fn main() {
    let child = Rc::new(Some(Shared));

    let strict_parent = StrictParent {
        child: Rc::clone(&child)
    };

    let lenient_parent = LenientParent {
        child
    };
}

Rust Playground

However, if Option<Rc<…>> works for your use-case, that might be more straightforward.

2 Likes

But then again, there's no good reason for that double indirection, either… Option<Rc<dyn Trait>> expresses the same thing (bonus: if the optional is none, then it doesn't allocate). Playground.

    let child: Option<Rc<Shared>> = Some(Rc::new(Shared));

    let strict_parent = StrictParent {
        child: child.clone(),
    };

    let lenient_parent = LenientParent {
        child: child.map(|rc| rc as _),
    };

Thanks steffahn and H2CO3!

Unfortunately neither of the options you proposed would work in my case, but I think that I might be able to rewrite the whole module so that only one struct needs to hold the child and the other one would call methods of the child through the parent.

Really appreciate you holding my hand like this, I've certainly learned a lot from you today!