How can I define a function with a parameter that can be multiple kinds of trait objects?
Asked Answered
A

1

14

I'm trying to define a function that will take a reference as a parameter, and call a generic method on the referenced object, passing in a concrete value. I need a way of requiring that the generic type of the parameter passed to my function is a trait of the concrete type that the function will use it with. I can't seem to work out how to do this.

A minimal example of the sort of thing I'm trying to achieve:

trait Vehicle {}
trait Floating {}

struct Boat;
impl Vehicle for Boat {}
impl Floating for Boat {}

fn main() {
    let mut a: Vec<Box<dyn Vehicle>> = vec![];
    populate(&mut a); // Does not compile

    let mut b: Vec<Box<dyn Floating>> = vec![];
    populate(&mut b); // Also does not compile
}

fn populate(receiver: &mut Vec<Box<Boat>>) { // What should I put here?
    receiver.push(Box::new(Boat{}));
}

Trying to compile this gives the following errors:

error[E0308]: mismatched types
  --> src/main.rs:10:14
   |
10 |     populate(&mut a); // Does not compile
   |              ^^^^^^ expected struct `Boat`, found trait object `dyn Vehicle`
   |
   = note: expected mutable reference `&mut std::vec::Vec<std::boxed::Box<Boat>>`
              found mutable reference `&mut std::vec::Vec<std::boxed::Box<dyn Vehicle>>`

error[E0308]: mismatched types
  --> src/main.rs:13:14
   |
13 |     populate(&mut b); // Also does not compile
   |              ^^^^^^ expected struct `Boat`, found trait object `dyn Floating`
   |
   = note: expected mutable reference `&mut std::vec::Vec<std::boxed::Box<Boat>>`
              found mutable reference `&mut std::vec::Vec<std::boxed::Box<dyn Floating>>`

I didn't expect this to compile, but I don't know how to change the signature of populate so that it will. I come from Java land, where I would achieve this using this using a bounded wildcard (e.g. void populate(List<? super Boat> receiver)), but I can't find anything to suggest that Rust offers equivalent semantics.

How might I go about fixing my definition of populate here?

I'm new to Rust, so bear with me if I'm completely barking up the wrong tree. I've searched around, and can't seem to find an example of how this pattern should be implemented.

Abrasion answered 9/7, 2018 at 14:14 Comment(4)
To be clear, you want populate to take a Vec<Trait> for any trait which Boat implements?Pimpernel
It would be Vec<Box<Trait>>, but essentially yes. I guess I would ideally expect it to also be able to take a Vec<Box<Boat>> as well.Abrasion
There's no way to take an arbitrary T, and ask if it's a trait implemented by Boat, so I don't think that as-is it is possible to write the populate function.Pimpernel
Thank you for what appears to be a unique question and including a succinct minimal reproducible example! Please continue to ask high-quality questions like this!Decalcify
D
8

Stable Rust

You can create and implement a trait for every unique trait object you are interested in:

trait Shipyard {
    fn construct(boat: Boat) -> Box<Self>;
}

impl Shipyard for Boat {
    fn construct(boat: Boat) -> Box<Self> {
        Box::new(boat)
    }
}

impl Shipyard for dyn Vehicle {
    fn construct(boat: Boat) -> Box<dyn Vehicle> {
        Box::new(boat) as Box<dyn Vehicle>
    }
}

impl Shipyard for dyn Floating {
    fn construct(boat: Boat) -> Box<dyn Floating> {
        Box::new(boat) as Box<dyn Floating>
    }
}

fn populate<T: ?Sized>(receiver: &mut Vec<Box<T>>)
where
    T: Shipyard,
{
    receiver.push(T::construct(Boat));
}

A macro can remove the duplication.

Nightly Rust

You can use the unstable CoerceUnsized trait:

#![feature(coerce_unsized)]

use std::ops::CoerceUnsized;

fn populate<T: ?Sized>(receiver: &mut Vec<Box<T>>)
where
    Box<Boat>: CoerceUnsized<Box<T>>,
{
    receiver.push(Box::new(Boat) as Box<T>);
}

Equivalently:

#![feature(unsize)]

use std::marker::Unsize;

fn populate<T: ?Sized>(receiver: &mut Vec<Box<T>>)
where
    Boat: Unsize<T>,
{
    receiver.push(Box::new(Boat) as Box<T>);
}

You can track their stabilization in issue 27732.

This code is only able to create a trait object, and cannot return the struct directly:

let mut b: Vec<Box<Boat>> = vec![];
populate(&mut b);
error[E0277]: the trait bound `Boat: std::marker::Unsize<Boat>` is not satisfied
  --> src/main.rs:17:5
   |
17 |     populate(&mut b);
   |     ^^^^^^^^ the trait `std::marker::Unsize<Boat>` is not implemented for `Boat`
   |
   = note: required because of the requirements on the impl of `std::ops::CoerceUnsized<std::boxed::Box<Boat>>` for `std::boxed::Box<Boat>`
note: required by `populate`
  --> src/main.rs:25:5
   |
25 | /     fn populate<T: ?Sized>(receiver: &mut Vec<Box<T>>)
26 | |     where
27 | |         Box<Boat>: CoerceUnsized<Box<T>>,
28 | |     {
29 | |         receiver.push(Box::new(Boat) as Box<T>);
30 | |     }
   | |_____^

To work around this, you can create a trait like we did for stable Rust, but this one can have a blanket implementation for all trait objects:

#![feature(unsize)]

use std::marker::Unsize;

trait Shipyard {
    fn construct(boat: Boat) -> Box<Self>;
}

impl Shipyard for Boat {
    fn construct(boat: Boat) -> Box<Self> {
        Box::new(boat)
    }
}

impl<U: ?Sized> Shipyard for U
where
    Boat: Unsize<U>,
{
    fn construct(boat: Boat) -> Box<Self> {
        Box::new(boat) as Box<U>
    }
}

fn populate<T: ?Sized>(receiver: &mut Vec<Box<T>>)
where
    T: Shipyard,
{
    receiver.push(T::construct(Boat));
}

Thanks to aturon for pointing me to these traits and to eddyb for reminding me that traits exist!

Decalcify answered 9/7, 2018 at 16:38 Comment(3)
Thank you so much for this. It's what I'd been looking for. Having poked around into the meaning of your proposed solution, I think I've come up with an alternative where clause that better expresses the bounds of the function: where Boat: Unsize<T>. This seems to work - any reason you can think of why it shouldn't be used?Abrasion
... I guess your version of the clause more directly expresses the bounds required for the implementation to work...Abrasion
@Abrasion no, I don't have a good sense of which one is more appropriate. It's probably something like From and Into though.Decalcify

© 2022 - 2024 — McMap. All rights reserved.