williamg
williamg

Reputation: 2758

Using traits as types in enums

Here's my code:

trait UnaryOperator {
    fn apply(&self, expr: Expression) -> Expression;
}

pub enum Expression {
    UnaryOp(UnaryOperator, Expression),
    Value(i64)
}

Which gives the following errors:

error: the trait 'core::marker::sized' is not implemented for type 'parser::UnaryOperator'
note: 'parser::UnaryOperator' does not have a constant size known at compile-time

I'm not sure how to accomplish what I want. I've tried:

trait UnaryOperator: Sized {
    ...
}

As well as

pub enum Expression {
    UnaryOp(UnaryOperator + Sized, Expression),
    ...
}

And neither solved the issue.

I've seen ways to possibly accomplish what I want with generics, but then it seems like two expressions with different operators would be different types, but that's not what I want. I want all expressions to be the same type regardless of what the operators are.

Upvotes: 1

Views: 572

Answers (1)

Shepmaster
Shepmaster

Reputation: 431809

Traits do not have a known size - they are unsized. To see why, check this out:

trait AddOne {
    fn add_one(&self) -> u8;
}

struct Alpha {
    a: u8,
}

struct Beta {
    a: [u8; 1024],
}

impl AddOne for Alpha {
    fn add_one(&self) -> { 0 }
}

impl AddOne for Beta {
    fn add_one(&self) -> { 0 }
}

Both Alpha and Beta implement AddOne, so how big should some arbitrary AddOne be? Oh, and remember that other crates may implement your trait sometime in the future.

That's why you get the first error. There are 3 main solutions (note that none of these solutions immediately fix your problem...):

  1. Use a Box<Trait>. This is kind-of-similar-but-different to languages like Java, where you just accept an interface. This has a known size (a pointer's worth) and owns the trait. This has the downside of requiring an allocation.
    trait UnaryOperator {
        fn apply(&self, expr: Expression) -> Expression;
    }

    pub enum Expression {
        UnaryOp(Box<UnaryOperator>, Expression),
        Value(i64)
    }
  1. Use a reference to the trait. This also has a known size (a pointer's worth two pointers' worth, see Matthieu M.'s comment). The downside is that something has to own the object and you need to track the lifetime:
    trait UnaryOperator {
        fn apply<'a>(&self, expr: Expression<'a>) -> Expression<'a>;
    }

    pub enum Expression<'a> {
        UnaryOp(&'a UnaryOperator, Expression<'a>),
        Value(i64)
    }
  1. Use a generic. This has a fixed size because each usage of the enum will have been specialized for the specific type. This has the downside of causing code bloat if you have many distinct specializations. Update As you point out, this means that Expression<A> and Expression<B> would have different types. Depending on your usage, this could be a problem. You wouldn't be able to easily create a Vec<Expression<A>> if you had both.
    trait UnaryOperator {
        fn apply<U>(&self, expr: Expression<U>) -> Expression<U>;
    }

    pub enum Expression<U>
        where U: UnaryOperator
    {
        UnaryOp(U, Expression<U>),
        Value(i64)
    }

Now, all of these fail as written because you have a recursive type definition. Let's look at this simplification:

enum Expression {
    A(Expression),
    B(u8),
}

How big is Expression? Well, it needs to have enough space to hold... an Expression! Which needs to be able to hold an Expression.... you see where this is going.

You need to add some amount of indirection here. Similar concepts to #1 and #2 apply - you can use a Box or a reference to get a fixed size:

enum Expression {
    A(Box<Expression>),
    B(u8),
}

Upvotes: 5

Related Questions