Downcasting trait object with generic inner field

I'm having some trouble understanding how to use trait objects to store a value of different possible types, and later figuring out what concrete type was actually stored.

I have a struct Outer that contains a generic inner field:

#[derive(Debug)]
struct Outer<T: InnerTrait> {
    inner: T,
}

trait InnerTrait: Any + Debug + 'static {}
impl InnerTrait for u32 {}
impl InnerTrait for u64 {}

So that this (obviously) works:

fn main() {
    let a: Outer<u32> = Outer{inner: 32u32};
    let b: Outer<u64> = Outer{inner: 64u64};

    // error[E0308]: `if` and `else` have incompatible types
    // let c = if true {
    //     Outer { inner: 32u32 }
    // } else {
    //     Outer { inner: 64u64 }
    // };
}

However, a and b have different types, so I couldn't store them in the same variable c.

To solve this, I introduce a marker trait OuterTrait so that I can store a Box<dyn OuterTrait>:

impl<T: InnerTrait> OuterTrait for Outer<T> {}

fn main() {
    let c: Box<dyn OuterTrait> = Box::new(Outer { inner: 23u32 });
    let d: Box<dyn OuterTrait> = Box::new(Outer { inner: 46u64 });
    let e: Box<dyn OuterTrait> = if true {
        Box::new(Outer { inner: 32u32 })
    } else {
        Box::new(Outer { inner: 64u64 })
    };
}

So this seems to work fine. Is this a reasonable approach? My initial try was to have e be of type Box<Outer<dyn InnerTrait>>, but that didn't work.

My issue then starts when I later want to figure out if e was a Outer<u32> or a Outer<u64>. I got this to work on nightly using the trait_upcasting feature:

#![feature(trait_upcasting)]

fn main() {
    let e: Box<dyn OuterTrait> = if true {
        Box::new(Outer { inner: 32u32 })
    } else {
        Box::new(Outer { inner: 64u64 })
    };

    if let Ok(down) = (e as Box<dyn Any + 'static>).downcast::<Outer<u32>>() {
        eprintln!("ok: {down:?}");
    } else {
        eprintln!("err: could not downcast to u32");
    }
    // prints: "ok: Outer { inner: 32 }"
}

But on stable I get the error:
error[E0658]: cannot cast `dyn OuterTrait` to `dyn Any`, trait upcasting coercion is experimental.

Is there a way of doing this on stable? I feel like I'm doing something wrong, first upcasting to just downcast straight away...


Edit: full code on the playground

not as I know. if the types you want to use is finite, I think you'd better use enum instead of trait object for this.

if trait object must be used, your next best solution I would say is something like

2 Likes

Box<Outer<dyn InnerTrait>> would work if you add ?Sized to the bounds for T in Outer's definition:

struct Outer<T: ?Sized> {

(Note that you don't need T: InnerTrait in the definition, but you can add it if you want)

2 Likes

The set of types is indeed finite, so I'll look more into the enum option if it fits my broader use case better. Thanks!

Thanks, changing the definition for Outer as you suggest I can assign values to Box<Outer<dyn InnerTrait>>:

#[derive(Debug)]
struct Outer<T: ?Sized> {
    #[allow(dead_code)]
    inner: T,
}

fn main() {
    let f: Box<Outer<dyn InnerTrait>> = Box::new(Outer { inner: 23u32 });
    let g: Box<Outer<dyn InnerTrait>> = Box::new(Outer { inner: 23u64 });
    let h: Box<Outer<dyn InnerTrait>> = if true {
        Box::new(Outer { inner: 32u32 })
    } else {
        Box::new(Outer { inner: 64u64 })
    };
}

But then I can't figure out how to do the downcasting:

    if let Ok(down) = (h as Box<dyn Any + 'static>).downcast::<Outer<u32>>() {
        eprintln!("ok: {down:?}");
    } else {
        eprintln!("err: could not downcast to u32");
    }

fails with error[E0277]: the size for values of type `(dyn InnerTrait + 'static)` cannot be known at compilation time, and

    if let Ok(down) = (h as Box<Outer<dyn Any + 'static>>).downcast::<Outer<u32>>() {
        eprintln!("ok: {down:?}");
    } else {
        eprintln!("err: could not downcast to u32");
    }

fails with error[E0599]: no method named `downcast` found for struct `Box<Outer<(dyn Any + 'static)>>` in the current scope.

Any idea?


full code on the playground

Is there any reason not to use Outer<Box<dyn InnerTrait>>?

1 Like

I'm worried that would hurt performance.

In my use case, Outer has a run() method that fetches data from a large chunk of memory in a loop, and feeds values to a process() method provided by the inner field. Changing the type of inner will then change the operation performed on the data.

Here's an example (playground):

use std::thread::{self, JoinHandle};

static LOTS_OF_DATA: &[u128] = &[42; 1024];

/// Outer implements the logic of fetching data
/// and delegates to inner for the actual processing
struct Outer<T> {
    inner: T,
}
impl<T> Outer<T> {
    fn run(mut self) -> Self
    where
        T: InnerTrait,
    {
        for &val in LOTS_OF_DATA {
            self.inner.process(val);
        }

        self
    }
    fn into_inner(self) -> T {
        self.inner
    }
}

/// Types implementing InnerTrait define data processing
trait InnerTrait {
    fn process(&mut self, val: u128);
}

/// A simple stateful data processor
struct Inner {
    sum: u128,
}
impl InnerTrait for Inner {
    fn process(&mut self, val: u128) {
        self.sum += val;
    }
}

fn main() {
    // Initialize processing
    let outer = Outer {
        inner: Inner { sum: 0 },
    };
    // Perform processing in separate thread, performance critical
    let handle: JoinHandle<Outer<Inner>> = thread::spawn(move || outer.run());

    // Access results, performance not so critical anymore
    let inner = handle.join().unwrap().into_inner();
    dbg!(inner.sum);
}

In practice, Outer::run() runs in a separate thread with std::thread::spawn(), and I would like to store a JoinHandle<Box<Outer<dyn InnerTrait>>> or similar instead of the JoinHandle<Outer<Inner>> in my example. This way I can choose which concrete type T: InnerTrait to use in the processing based on user input.

My idea is that this way the performance cost of dynamic dispatch and Boxing will be paid at the end of the processing when I join the tread and access the data. In the hot loop, Outer calls into a concrete and monomorphised T.

If instead I had a JoinHandle<Outer<Box<dyn InnerTrait>>>, then Outer would have to call into a Box<dyn InnerTrait> with dynamic dispatch inside the hot loop.

But I'm wondering, does this make any sense? Or have I misunderstood how dynamic dispatch works?

Do you have to downcast the inner trait? How different are the results--could you add a sum method to the InnerTrait definition, such that regardless of what InnerTrait you use sum() always returns a u128?

(In general I think that downcasting trait objects indicates I've made a mistake in my design and I should reach instead for a tool designed for that purpose, often enum).

2 Likes

Here's one approach.

...but I agree with the others that if you're downcasting to a set of known types, you probably wanted an enum instead.

You can avoid the nightly feature with enough boilerplate if you decide to stick with downcasting.

2 Likes

I see, it makes sense. By using Box<dyn Trait> I erase information about the concrete type, but if later I feel the need to downcast then perhaps the information shouldn't have been erased in the first place. So either use an enum to keep the type information around, or expand the functionality of the trait to avoid the need for downcasting.

Anyway, thanks for all the help and suggestions on using dyn and downcasting. I feel like I understand this corner of Rust a little bit better now.

1 Like

That's usually the way to go if you want to keep the set of types extensible. (An enum is nice but it's usually for describing a set of closely-related things.) If you have a trait object, and some operation depends on the type, then that shouldn't be achieved by downcasting and a huge ladder of if-else ifs. If the operation depends on the type, that's a clear indication that it should be part of the trait as a method.

4 Likes