Storing object with a method called with a trait type parameter

I have two hierarchies of objects: let's call them params and evaluators:

trait MyParamTrait { fn compute_param(&self) -> f64; }
trait MyEvalTrait { fn eval(&self, x: &impl MyParamTrait) -> f64; }

I want the eval() method to accept any of my parameter types. I defined a method that accepts &impl MyParamTrait) argument. The code looks as below:

trait MyParamTrait { fn compute_param(&self) -> f64; }

struct Param1;
impl MyParamTrait for Param1 { fn compute_param(&self) -> f64 { 2.0 } }
struct Param2;
impl MyParamTrait for Param2 { fn compute_param(&self) -> f64 { 4.0 } }

trait MyEvalTrait { fn eval(&self, x: &impl MyParamTrait) -> f64; }

struct Eval1;
impl MyEvalTrait for Eval1 {  fn eval(&self, x: &impl MyParamTrait) -> f64 { x.compute_param() * 2.0 } }

pub fn main(){

    let p1 = Param1{};
    let _p2 = Param2{};
    let e = Eval1{};
    println!("{}", e.eval(&p1));
    // so far, so good ... but I need to store MyObjects
    let mut all_my_obj: Vec<Box<dyn MyEvalTrait>> = Vec::new();
    all_my_obj.push(Box::new(e));
}

Unfortunately I also need to store objects that implement MyEvalTrait in a vector, and this is the moment when problem starts. Compiler tells me:

error[E0038]: the trait `MyEvalTrait` cannot be made into an object
  --> src/main.rs:27:25
   |
27 |     let mut all_my_obj: Vec<Box<dyn MyEvalTrait>> = Vec::new();
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyEvalTrait` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/main.rs:10:24
   |
10 | trait MyEvalTrait { fn eval(&self, x: &impl MyParamTrait) -> f64; }
   |       -----------      ^^^^ ...because method `eval` has generic type parameters
   |       |
   |       this trait cannot be made into an object...
   = help: consider moving `eval` to another trait

I can understand the reason; MyEvalTrait is abstract and it can't be box'ed. But how can I make this working? I found a solution with generic:

trait MyEvalTrait<P> { fn eval(&self, x: &P) -> f64; }

struct Eval1;
impl MyEvalTrait<Param1> for Eval1 { fn eval(&self, x: &Param1) -> f64 { x.compute_param() * 2.0 } }

pub fn main(){

    let p1 = Param1{};
    let _p2 = Param2{};
    let e = Eval1{};
    println!("{}", e.eval(&p1));
    // so far, so good ... but I need to store MyObjects
    let mut all_my_obj: Vec<Box<dyn MyEvalTrait<Param1>>> = Vec::new();
    all_my_obj.push(Box::new(e));
}

and this works just fine. I'd like to avoid that however, because in reality I have a few methods in my MyEvalTrait, say another_eval(y: &impl OtherParamTrait) which requires parameter of another type OtherParamTrait. When I'm using generics, the trait MyEvalTrait<P1, P2, P3 ...> depends on several generic parameters which looks clumsy and is difficult to follow.

I wonder if there is another solution.

One option is to change eval to

fn eval(&self, x: &dyn MyParamTrait) -> f64;

It’s slightly more overhead because the function calls to x.compute_param() would become “dynamic”[1]; but in most cases, the overhead is irrelevant anyways. At least unless compute_param is a tiny function, and called a bunch of times in a tight loop.


  1. using function pointers read from a vtable, instead of statically dispatched (and possibly even inlined) calls ↩︎

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.