20
votes

I want to use trait objects in a Vec. In C++ I could make a base class Thing from which is derived Monster1 and Monster2. I could then create a std::vector<Thing*>. Thing objects must store some data e.g. x : int, y : int, but derived classes need to add more data.

Currently I have something like

struct Level {
    // some stuff here
    pub things: Vec<Box<ThingTrait + 'static>>,
}

struct ThingRecord {
    x: i32,
    y: i32,
}

struct Monster1 {
    thing_record: ThingRecord,
    num_arrows: i32,
}

struct Monster2 {
    thing_record: ThingRecord,
    num_fireballs: i32,
}

I define a ThingTrait with methods for get_thing_record(), attack(), make_noise() etc. and implement them for Monster1 and Monster2.

1
if your monsters are mostly known in advance (i.e. your not creating a game engine allowing everyone to create a new monster) you could alternatively work with an enumPaolo Falabella
See also the discussion on reddit. (BTW, if cross-posting a question, it's generally polite to at least link between them so that e.g. interested people don't miss out on discussion.)huon
Ok. I am torn between using Traits and a method that returns the shared data... or just using an enum for everything. I am thinking the former is the lesser of two evils. If inheritance is going to be added to the language, what is it going to look like? Apart from this there is hardly anything I miss from C++. A breath of fresh air.stevenkucera
Rust blog covers this: Abstraction without overhead: traits -- an excellent read IMHO.legends2k

1 Answers

27
votes

Trait objects

The most extensible way to implement a heterogeneous collection (in this case a vector) of objects is exactly what you have:

Vec<Box<dyn ThingTrait + 'static>>

Although there are times where you might want a lifetime that's not 'static, so you'd need something like:

Vec<Box<dyn ThingTrait + 'a>>

You could also have a collection of references to traits, instead of boxed traits:

Vec<&dyn ThingTrait>

An example:

trait ThingTrait {
    fn attack(&self);
}

impl ThingTrait for Monster1 {
    fn attack(&self) {
        println!("monster 1 attacks")
    }
}

impl ThingTrait for Monster2 {
    fn attack(&self) {
        println!("monster 2 attacks")
    }
}

fn main() {
    let m1 = Monster1 {
        thing_record: ThingRecord { x: 42, y: 32 },
        num_arrows: 2,
    };

    let m2 = Monster2 {
        thing_record: ThingRecord { x: 42, y: 32 },
        num_fireballs: 65,
    };

    let things: Vec<Box<dyn ThingTrait>> = vec![Box::new(m1), Box::new(m2)];
}

Box<dyn SomeTrait>, Rc<dyn SomeTrait>, &dyn SomeTrait, etc. are all trait objects. These allow implementation of the trait on an infinite number of types, but the tradeoff is that it requires some amount of indirection and dynamic dispatch.

See also:

Enums

As mentioned in the comments, if you have a fixed number of known alternatives, a less open-ended solution is to use an enum. This doesn't require that the values be Boxed, but it will still have a small amount of dynamic dispatch to decide which concrete enum variant is present at runtime:

enum Monster {
    One(Monster1),
    Two(Monster2),
}

impl Monster {
    fn attack(&self) {
        match *self {
            Monster::One(_) => println!("monster 1 attacks"),
            Monster::Two(_) => println!("monster 2 attacks"),
        }
    }
}

fn main() {
    let m1 = Monster1 {
        thing_record: ThingRecord { x: 42, y: 32 },
        num_arrows: 2,
    };

    let m2 = Monster2 {
        thing_record: ThingRecord { x: 42, y: 32 },
        num_fireballs: 65,
    };

    let things = vec![Monster::One(m1), Monster::Two(m2)];
}

See also: