Home > Mobile >  Casting a struct to a trait object with associated types in Rust
Casting a struct to a trait object with associated types in Rust

Time:10-25

In rust, you can have a trait, implement it into a struct, and upcast your struct to a trait object :

trait T {}

struct S {}
impl T for S {}

fn main() {
    let s: S = S {};
    let s_as_t: &dyn T = &s;
}

This is an incredibly useful feature, because if I have multiple objects which all implement the trait T, I can now put them all in a single array of type Vec<Box<dyn T>>, and define global behaviors really easily by calling a function on each element.

BUT

How do I do the same thing when my original trait also has an associated type ?

This works really well, no pb :

trait T_Subtype {}

trait T {
    type subtype: T_Subtype;
}

struct S {}
impl T_Subtype for S {}
impl T for S {
    type subtype = S;
}

fn main() {
    let s: S = S {};
    let s_as_t: &dyn T<subtype = S> = &s;
}

But I can't find any way to upcast the associated type, the following code cannot compile :

trait T_Subtype {}

trait T {
    type subtype: T_Subtype;
}

struct S {}
impl T_Subtype for S {}
impl T for S {
    type subtype = S;
}

fn main() {
    let s: S = S {};
    let s_as_t: &dyn T<subtype = dyn T_Subtype> = &s; // only line that changes
}

Without this feature, I cannot put (this is an illustration) multiple structs S1 S2 and S3, that all implement T but might have a different subtype, in a single array, and I have to define global behaviors for each subtype, making it really hard to maintain (especially if there are multiple subtypes), even though the function I want to call on all of them is defined !

CodePudding user response:

This isn't possible.

Consider the following code:

trait SubtypeTrait {}

trait T {
    type Subtype: SubtypeTrait;
    fn foo(arg: &<Self as T>::Subtype);
}

struct S {
    val: i32,
}
impl SubtypeTrait for S {}
impl T for S {
    type Subtype = S;
    fn foo(arg: &S) {
        println!("{}", arg.val);
    }
}

struct X {}
impl SubtypeTrait for X {}
impl T for X {
    type Subtype = S;
    fn foo(arg: &S) {
        println!("{}", arg.val);
    }
}

// Everything compiles except `main`
fn main() {
    let x: X = X {};
    let x_as_t: &dyn T<Subtype = dyn SubtypeTrait> = &x;
    let s: S = S {};
    // Attempts to access `arg.val`...
    // but `x` doesn't have `val`!
    x_as_t.foo(&x);
}

The problem is that a trait bound on a placeholder type doesn't restrict the operations on the placeholder type, it only restricts the type itself. Once you specify the type you can choose any operation from the type that you want.

CodePudding user response:

Ok, so this problem hurt my brain for a while. Apparently it is impossible to do, but I still felt like there should be a way. And here it is :

// we define the traits and sub-traits, like we did before
trait T2 {
    fn hello2(&self);
}
trait T1 {
    type SubT: T2;

    fn hello1(&self) -> Self::SubT;
}

// first implementation of T1 (maybe somewhere else in the code)
struct S1;
impl T2 for S1 {
    fn hello2(&self) {
        println!("hello2 from S1");
    }
}
impl T1 for S1 {
    type SubT = S1;

    fn hello1(&self) -> Self::SubT {
        println!("hello from s1");
        S1 {}
    }
}

// second implementation of T1 (maybe somewhere else in the code)
struct S2;
impl T2 for S2 {
    fn hello2(&self) {
        println!("hello2 from S2");
    }
}
impl T1 for S2 {
    type SubT = S2;

    fn hello1(&self) -> Self::SubT {
        println!("hello from s2");
        S2 {}
    }
}

// where the magic happens !!
// we use a blanket implementation to make it automatic
trait T1Blanket {
    fn hello1(&self) -> Box<dyn T2>;
}
impl<S: T1> T1Blanket for S where S::SubT: 'static {
    fn hello1(&self) -> Box<dyn T2> {
        Box::from(self.hello1()) as Box<dyn T2>
    }
}

// and now we can use it easily
fn main() {
    let s1 = S1 {};
    let s2 = S2 {};

    let array: Vec<Box<dyn T1Blanket>> = vec![Box::from(s1), Box::from(s2)];
    for element in array {
        let h = element.hello1(); // prints "hello from s1" and "hello from s2" successively
        h.hello2(); // you can also call the other trait method's
    }
}

This works beautifully, and the best part is once you created the blanket implementation, you don't have to touch anything ! You can create as many structs implementing T1 and T2 as you want, and they won't have any restriction put on them (they won't need special function to be implemented either).

You can even automate this work using macros if you have many of such traits : this crate apparently does it for you.

I'm still incredibly surprised that this isn't included directly in the default behaviors of trait objects which have associated types. As long as the trait implementation isn't needed to call the function, using trait objects shouldn't be a problem.

  • Related