Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I cast between two traits?

Tags:

rust

traits

Is there a way to cast from one trait to another?

I have the traits Foo and Bar and a Vec<Box<dyn Foo>>. I know some of the items in the Vec implement the Bar trait, but is there any way I could target them?

I don't understand if this is possible or not.

trait Foo {
    fn do_foo(&self);
}

trait Bar {
    fn do_bar(&self);
}

struct SomeFoo;

impl Foo for SomeFoo {
    fn do_foo(&self) {
        println!("doing foo");
    }
}

struct SomeFooBar;

impl Foo for SomeFooBar {
    fn do_foo(&self) {
        println!("doing foo");
    }
}

impl Bar for SomeFooBar {
    fn do_bar(&self) {
        println!("doing bar");
    }
}

fn main() {
    let foos: Vec<Box<dyn Foo>> = vec![Box::new(SomeFoo), Box::new(SomeFooBar)];

    for foo in foos {
        foo.do_foo();

        // if let Some(val) = foo.downcast_whatever::<Bar>() {
        //     val.bar();
        // }
    }
}

[Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8b637bddc4fc923ce705e84ad1d783d4)
like image 390
Christoph Avatar asked Dec 22 '15 15:12

Christoph


2 Answers

No. There is no way to cast between two unrelated traits. To understand why, we have to understand how trait objects are implemented. To start with, let's look at TraitObject.

TraitObject is a reflection of how trait objects are actually implemented. They are composed of two pointers: data and vtable. The data value is just a reference to the original object:

#![feature(raw)]

use std::{mem, raw};

trait Foo {}
impl Foo for u8 {}

fn main() {
    let i = 42u8;
    let t = &i as &dyn Foo;
    let to: raw::TraitObject = unsafe { mem::transmute(t) };

    println!("{:p}", to.data);
    println!("{:p}", &i);
}

vtable points to a table of function pointers. This table contains references to each implemented trait method, ordered by some compiler-internal manner.

For this hypothetical input

trait Foo {
    fn one(&self);
}

impl Foo for u8 {
    fn one(&self) { println!("u8!") }
}

The table is something like this pseudocode

const FOO_U8_VTABLE: _ = [impl_of_foo_u8_one];

A trait object knows a pointer to the data and a pointer to a list of methods that make up that trait. From this information, there is no way to get any other piece of data.

Well, almost no way. As you might guess, you can add a method to the vtable that returns a different trait object. In computer science, all problems can be solved by adding another layer of indirection (except too many layers of indirection).

See also:

  • Why doesn't Rust support trait object upcasting?

But couldn't the data part of the TraitObject be transmuted to the struct

Not safely, no. A trait object contains no information about the original type. All it has is a raw pointer containing an address in memory. You could unsafely transmute it to a &Foo or a &u8 or a &(), but neither the compiler nor the runtime data have any idea what concrete type it originally was.

The Any trait actually does this by also tracking the type ID of the original struct. If you ask for a reference to the correct type, the trait will transmute the data pointer for you.

Is there a pattern other than the one I described with my FooOrBar trait to handle such cases where we need to iterate over a bunch of trait objects but treat some of them slightly different?

  • If you own these traits, then you can add as_foo to the Bar trait and vice versa.

  • You could create an enum that holds either a Box<dyn Foo> or a Box<dyn Bar> and then pattern match.

  • You could move the body of bar into the body of foo for that implementation.

  • You could implement a third trait Quux where calling <FooStruct as Quux>::quux calls Foo::foo and calling <BarStruct as Quux>::quux calls Bar::foo followed by Bar::bar.

like image 159
Shepmaster Avatar answered Sep 29 '22 10:09

Shepmaster


so... I don't think this is exactly what you want, but it's the closest I can get.

// first indirection: trait objects
let sf: Box<Foo> = Box::new(SomeFoo);
let sb: Box<Bar> = Box::new(SomeFooBar);

// second level of indirection: Box<Any> (Any in this case
// is the first Box with the trait object, so we have a Box<Box<Foo>>
let foos: Vec<Box<Any>> = vec![Box::new(sf), Box::new(sb)];

// downcasting to the trait objects
for foo in foos {
    match foo.downcast::<Box<Foo>>() {
        Ok(f) => f.do_foo(),
        Err(other) => {
            if let Ok(bar) = other.downcast::<Box<Bar>>() {
                    bar.do_bar();
            }
        }
    }
}

note that we can call SomeFooBar as a Box<Bar> only because we stored it as a Box<Bar> in the first place. So this is still not what you want (SomeFooBar is a Foo too, but you can't convert it to a Box<Foo> any longer, so we're not really converting one trait to the other)

like image 28
Paolo Falabella Avatar answered Sep 29 '22 12:09

Paolo Falabella