Search code examples
rustpolymorphismtraitsdispatchtrait-objects

Rust: polymorphic calls for structs in a vector


I'm a complete newbie in Rust and I'm trying to get some understanding of the basics of the language.

Consider the following trait

trait Function {
    fn value(&self, arg: &[f64]) -> f64;
}

and two structs implementing it:

struct Add {}

struct Multiply {}

impl Function for Add {
    fn value(&self, arg: &[f64]) -> f64 {
        arg[0] + arg[1]
    }
}

impl Function for Multiply {
    fn value(&self, arg: &[f64]) -> f64 {
        arg[0] * arg[1]
    }
}

In my main() function I want to group two instances of Add and Multiply in a vector, and then call the value method. The following works:

fn main() {
    let x = vec![1.0, 2.0];
    let funcs: Vec<&dyn Function> = vec![&Add {}, &Multiply {}];

    for f in funcs {
        println!("{}", f.value(&x));
    }
}

And so does:

fn main() {
    let x = vec![1.0, 2.0];
    let funcs: Vec<Box<dyn Function>> = vec![Box::new(Add {}), Box::new(Multiply {})];

    for f in funcs {
        println!("{}", f.value(&x));
    }
}

Is there any better / less verbose way? Can I work around wrapping the instances in a Box? What is the takeaway with trait objects in this case?


Solution

  • Is there any better / less verbose way?

    There isn't really a way to make this less verbose. Since you are using trait objects, you need to tell the compiler that the vectors's items are dyn Function and not the concrete type. The compiler can't just infer that you meant dyn Function trait objects because there could have been other traits that Add and Multiply both implement.

    You can't abstract out the calls to Box::new either. For that to work, you would have to somehow map over a heterogeneous collection, which isn't possible in Rust. However, if you are writing this a lot, you might consider adding helper constructor functions for each concrete impl:

    impl Add {
        fn new() -> Add {
            Add {}
        }
    
        fn new_boxed() -> Box<Add> {
            Box::new(Add::new())
        }
    }
    

    It's idiomatic to include a new constructor wherever possible, but it's also common to include alternative convenience constructors.

    This makes the construction of the vector a bit less noisy:

    let funcs: Vec<Box<dyn Function>> = vec!(Add::new_boxed(), Multiply::new_boxed()));
    

    What is the takeaway with trait objects in this case?

    There is always a small performance hit with using dynamic dispatch. If all of your objects are the same type, they can be densely packed in memory, which can be much faster for iteration. In general, I wouldn't worry too much about this unless you are creating a library crate, or if you really want to squeeze out the last nanosecond of performance.