We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Trait Objects and Dispatch step 4 of 10
Enum dispatch: the third option
Most Rust tutorials present exactly two ways to hold a mixed bag of types:
generics (fast, but homogeneous) and Box<dyn Trait> (heterogeneous, but
indirect). There is a third, it is systematically under-taught, and on the
measurements from the previous item it wins outright:
| shape (implementation chosen at run time) | time |
|---|---|
opaque &dyn Op, 5M elements |
0.904 ms |
enum + match |
0.250 ms |
| statically-known concrete call | 0.2499 ms |
Enum dispatch was indistinguishable from the fully static case and 3.6×
faster than dyn. It also allocates nothing.
The idea is simple to the point of feeling like cheating: instead of erasing
the type behind a vtable, name every possibility in one enum and implement
the trait for the enum by delegating each method to a match.
enum AnyShape { Circle(Circle), Rect(Rect), Triangle(Triangle) }
impl Shape for AnyShape {
fn area(&self) -> f64 {
match self {
AnyShape::Circle(c) => c.area(),
...
}
}
}
Now Vec<AnyShape> is a flat, contiguous array of values. No Box. No heap
allocation per element. No vtable load. The match compiles to a jump table
or a handful of predictable branches, and — crucially — the optimiser can see
every arm, so it can inline all of them.
In the wider ecosystem a macro crate (enum_dispatch) generates this
boilerplate for you. There are no external crates here, so you write it by
hand, which is better pedagogy anyway: you will see exactly how much code you
are trading for the speed. (macro_rules! is available and can generate the
delegating arms — a nice payoff once you reach the macros track.)
The trade, stated honestly
-
The enum is as big as its largest variant, plus a tag. A
Vecof them wastes space on the small ones. If one variant is enormous, box that variant — clippy’slarge_enum_variantwill tell you. -
It closes the set. Nobody outside your crate can add a shape. That is
the entire reason
dynexists: plugins, user extension,Box<dyn Error>. If the set of implementors is genuinely open, you needdyn. If it is five things you wrote yourself, an enum is almost always better. -
Adding a variant is a breaking change for every
match— which is usually a feature. E0004 will find every place you forgot.
What to write
A Shape trait and three concrete shapes are given. You write:
pub fn make(kind: u8, a: f64, b: f64) -> AnyShape
impl Shape for AnyShape { ... }
pub fn total_area(shapes: &[AnyShape]) -> f64
make selects on kind % 3: 0 → Circle { r: a }, 1 →
Rect { w: a, h: b }, 2 → Triangle { b: a, h: b }.
total_area sums in a specified order: start at 0.0 and add each
shape’s area in slice order, left to right. Floating-point addition is not
associative, so an unspecified order would not be reproducible — a point the
performance track will hammer on.
The gate that matters
One test builds 100 000 shapes and counts heap allocations during
construction. The vector itself is pre-sized by the harness before counting
starts, so the budget is zero allocations. A Vec<Box<dyn Shape>> design
allocates 100 000 times and fails on the spot. That number is deterministic —
no timing, no flakiness, and it is the honest way to prove the structural
claim.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.