3

I'm new to Rust and have seen some examples of people using Box to allow pushing many types that implement a certain Trait onto a Vec. When using a Trait with Generics, I have run into an issue.

error[E0038]: the trait `collision::collision_detection::Collidable` cannot be made into an object
  --> src/collision/collision_detection.rs:19:5
   |
19 |     collidables: Vec<Box<Collidable<P, M>>>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `collision::collision_detection::Collidable` cannot be made into an object
   |
   = note: method `get_ncollide_shape` has generic type parameters

error: aborting due to previous error

error: Could not compile `game_proto`.

To learn more, run the command again with --verbose.

Here is my code

extern crate ncollide;
extern crate nalgebra as na;

use self::ncollide::shape::Shape;
use self::ncollide::math::Point;
use self::ncollide::math::Isometry;
use self::na::Isometry2;

pub trait Collidable<P: Point, M> {
    fn get_ncollide_shape<T: Shape<P, M>>(&self) -> Box<T>;
    fn get_isometry(&self) -> Isometry2<f64>;
}

pub struct CollisionRegistry<P, M>
where
    P: Point,
    M: Isometry<P>,
{
    collidables: Vec<Box<Collidable<P, M>>>,
}

impl<P: Point, M: Isometry<P>> CollisionRegistry<P, M> {
    pub fn new() -> Self {
        let objs: Vec<Box<Collidable<P, M>>> = Vec::new();
        CollisionRegistry { collidables: objs }
    }

    pub fn register<D>(&mut self, obj: Box<D>)
    where
        D: Collidable<P, M>,
    {
        self.collidables.push(obj);
    }
}

I'm trying to use collidables as a list of heterogenous game objects that will give me ncollide compatible Shapes back to feed into the collision detection engine.

EDIT: To clear up some confusion. I'm not trying to construct and return an instance of a Trait. I'm just trying to create a Vec that will allow any instance of the Collidable trait to be pushed onto it.

  • Possible duplicate of [Why can a trait not construct itself?](https://stackoverflow.com/questions/38159771/why-can-a-trait-not-construct-itself) There may be better duplicates, but similar. – loganfsmyth Sep 06 '17 at 00:43
  • @loganfsmyth That's not what I'm trying to do. I've read through most of the examples that are similar to this and have gotten them to work by using Vec>. But when I use a trait that has a generic type like Vec>>, suddenly I get this error. – Timothy Bess Sep 06 '17 at 00:53
  • What are you expecting `get_ncollide_shape` to do? The error is because `Box>` means that essentially you've erased all data about an object except that it implements a trait. In that context, `fn get_ncollide_shape>(&self) -> Box;` does not make sense, because there would be no way to call it. There are an infinite number of versions of that function because it is parametric on `T`, so there's no way to use that function, because it needs to decide at runtime which version to call, and the options are known. – loganfsmyth Sep 06 '17 at 01:22
  • @loganfsmyth I'm not sure what you mean really. That part just returns any object implementing the Shape trait. That part of the code compiles and seems fine so far. In the error message, it references line 19 (in the struct definition) as the problem. Have any ideas about that part? – Timothy Bess Sep 06 '17 at 02:06
  • The error is on line 19 because that is where you declare a boxed version of the trait. The error says "note: method `get_ncollide_shape` has generic type parameters" because generic parameters like `get_ncollide_shape>` are not allowed on traits that will be boxed. Rust is a compiled language, so it needs to be able to create code to do what you're asking, and it can't do what you're asking. – loganfsmyth Sep 06 '17 at 02:15

1 Answers1

5

Rust is a compiled language, so when it compiles your code, it needs to know all of the information it might need to generate machine code.

When you say

trait MyTrait {
  fn do_thing() -> Box<u32>;
}

struct Foo {
   field: Box<MyTrait>
}

you are telling Rust that Foo will contain a box containing anything implementing MyTrait. By boxing the type, the compiler will erase any additional data about the data type that isn't covered by the trait. These trait objects are implemented as a set of data fields and a table of functions (called a vtable) that contains the functions exposed by the trait, so they can be called.

When you change

fn do_thing() -> Box<u32>;

to

fn do_thing<T>() -> Box<T>;

it may look similar, but the behavior is much different. Let's take a normal function example

fn do_thing<T>(val: T) { }

fn main() {
  do_thing(true);
  do_thing(45 as u32);
}

the compiler performs what is a called monomorphization, which means your code in the compiler becomes essentially

fn do_thing_bool(val: bool) { }
fn do_thing_num(val: u32) { }

fn main() {
  do_thing_bool(true);
  do_thing_num(45 as u32);
}

The key thing to realize is that you are asking it to do the same thing for your trait. The problem is that the compiler can't do it. The example above relies on knowing ahead of time that do_thing is called with a number in one case and a boolean in another, and it can know with 100% certainty that those are the only two ways the function is used.

With your code

trait MyTrait {
  fn do_thing<T>() -> Box<T>;
}

the compiler does not know what types do_thing will be called with, so it has no way to generate functions you'd need to call. To do that, wherever you convert the struct implementing Collidable into a boxed object it would have to know every possible return type get_ncollide_shape could have, and that is not supported.

Other links for this:

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251