10

I have a struct that mostly encapsulates a vector:

struct Group<S> {
    elements: Vec<S>
}

I also have a simple trait which is also implemented for other structs:

trait Solid {
    fn intersect(&self, ray: f32) -> f32;
}

I want to implement Solid for Group, but I want to be able to use Group both for lists of the same implementation of Solid and for lists of mixed implementations of Solid. Basically I want to use both Group<Box<Solid>> and Group<Sphere> (Sphere implements Solid).

Currently I am using something like this:

impl Solid for Group<Box<Solid>> {
    fn intersect(&self, ray: f32) -> f32 {
        //do stuff
    }
}

impl<S: Solid> Solid for Group<S> {
    fn intersect(&self, ray: f32) -> f32 {
        //do the same stuff, code copy-pasted from previous impl
    }
}

This works, but having line-for-line the same code twice can't be the idiomatic solution. I must be missing something obvious?

In my case I measure a notable performance difference between both trait implementations, so always using Group<Box<Solid>> isn't a great option.

Shepmaster
  • 274,917
  • 47
  • 731
  • 969
Jannis Froese
  • 1,167
  • 1
  • 8
  • 22

1 Answers1

12

Implement your trait for all Box<S> where S implements your trait. Then you can delegate to the existing implementation:

impl<S: Solid + ?Sized> Solid for Box<S> {
    fn intersect(&self, ray: f32) -> f32 {
        (**self).intersect(ray)
        // Some people prefer this less-ambiguous form
        // S::intersect(self, ray)
    }
}

You'll also find that it can be useful to do the same for references:

impl<S: Solid + ?Sized> Solid for &'_ S {
    fn intersect(&self, ray: f32) -> f32 {
        (**self).intersect(ray)
        // Some people prefer this less-ambiguous form
        // S::intersect(self, ray)
    }
}

All together:

trait Solid {
    fn intersect(&self, ray: f32) -> f32;
}

impl<S: Solid + ?Sized> Solid for Box<S> {
    fn intersect(&self, ray: f32) -> f32 {
        (**self).intersect(ray)
        // S::intersect(self, ray)
    }
}

impl<S: Solid + ?Sized> Solid for &'_ S {
    fn intersect(&self, ray: f32) -> f32 {
        (**self).intersect(ray)
        // S::intersect(self, ray)
    }
}

struct Group<S>(Vec<S>);

impl<S: Solid> Solid for Group<S> {
    fn intersect(&self, _ray: f32) -> f32 {
        42.42
    }
}

struct Point;

impl Solid for Point {
    fn intersect(&self, _ray: f32) -> f32 {
        100.
    }
}

fn main() {
    let direct = Group(vec![Point]);
    let boxed = Group(vec![Box::new(Point)]);
    let pt = Point;
    let reference = Group(vec![&pt]);

    let mixed: Group<Box<dyn Solid>> = Group(vec![
        Box::new(direct),
        Box::new(boxed),
        Box::new(Point),
        Box::new(reference),
    ]);

    mixed.intersect(1.0);
}

The ?Sized bound allows the S to not have a size known at compile time. Importantly, this allows you to pass in trait objects such as Box<dyn Solid> or &dyn Solid as the type Solid does not have a known size.

See also:

Shepmaster
  • 274,917
  • 47
  • 731
  • 969
  • 1
    @CarlLevasseur I've updated the existing sentence about `?Sized`; is it any clearer? – Shepmaster Sep 23 '17 at 14:17
  • Yes, so if `Box` is a `Solid`, then to use it in a function that takes a `&Solid`, we would have 2 "redirections", does that have an impact on performances ? Also, when is it useful to implement it for references ? – Carl Levasseur Sep 23 '17 at 14:38
  • @CarlLevasseur *does that have an impact on performance* — that depends on a lot of things (frequency of calls, compiler optimizations, monomorphization, etc.). The **only** answer for that is to profile to be sure. *when is it useful to implement it for references* — whenever you have a trait object reference instead of a boxed trait object ([example](https://stackoverflow.com/a/28220053/155423))... I don't think I fully understand the question. – Shepmaster Sep 23 '17 at 14:44
  • Why is the '(**self).intersect(ray)' part able to compile? I assume that '**self' is '?Sized', which may not necessarily be able to live on stack. – Garlic Xu Aug 06 '20 at 15:07
  • 1
    @GarlicXu it doesn't need to live on the stack though. The compiler will automatically insert a reference when the method call is made: [What are Rust's exact auto-dereferencing rules?](https://stackoverflow.com/q/28519997/155423) – Shepmaster Aug 06 '20 at 15:08
  • Why is `S::intersect(self, ray)` not resolved as a recursive call? Given that `self` has type `&&S`, and the `impl ... for &'_ S` block's `intersect` method also wants `self: &&S`, why does this method not take priority according to the rules? – Garlic Xu Aug 07 '20 at 03:40
  • @GarlicXu because `S` is a specific type. Our method is defined on `&S`, so `S::intersect` **must** refer to the dereferenced version. – Shepmaster Aug 17 '20 at 12:48