Skip to content

← Trait Objects and Dispatch step 1 of 10

Medium Primitives

Box<dyn Trait>: trait objects, DSTs and dynamic dispatch

Everything you have written with traits so far has been static dispatch: a generic fn draw<S: Shape>(s: &S) gets one compiled copy per concrete S, and the call is resolved before your program runs. That is fast, and it is useless the moment you want a heterogeneous collection — a list holding a circle and a rectangle and a thing a plugin registered five minutes ago.

Vec<S> cannot do that: every element of a Vec<S> is the same S. This is the problem dyn Trait exists to solve, and it is the Rust answer to Java’s List<Shape>.

Why the Box is mandatory, not decoration

dyn Op is a dynamically sized type (a DST). It means “some value, somewhere, that implements Op“ — and different implementors have different sizes, so the compiler cannot tell you size_of::<dyn Op>(). There is no answer.

Every generic parameter in Rust carries an implicit Sized bound. When you write

struct Vec<T> { ... }

you are really writing struct Vec<T: Sized>. Rust adds it for you, because 99% of the time you want it, and the escape hatch is the odd-looking T: ?Sized (“T may or may not be sized”). Vec<dyn Op> therefore fails with E0277: the size for values of type dyn Op cannot be known at compilation time.

Put it behind a pointer and the problem evaporates, because pointers have a known size. Box<dyn Op> is a fat pointer: two machine words, one pointing at the data and one pointing at a vtable — a small static table of function pointers for that concrete type’s impl Op. Calling op.apply(x) loads the function pointer out of the vtable and calls it. That indirection is what “dynamic dispatch” means.

The lint you must un-learn

Earlier tracks taught you to fear Vec<Box<T>> — clippy’s vec_box lint fires on Vec<Box<String>> and tells you the Box is a pointless extra allocation and indirection. vec_box does not fire on Vec<Box<dyn Trait>>, and it is right not to: it only fires when T is Sized, which is exactly the case where the Box was redundant. Here the Box is load-bearing. If a reviewer tells you to remove it, they are wrong.

What to write

A trait and three implementors are given. You write three things.

pub fn build(spec: &[(String, i64)]) -> Vec<Box<dyn Op>>

Turn a specification into a heterogeneous vector of operations. The names are "add", "mul" and "clamp" (which does x.min(n)). Any other name is skipped entirely — it contributes no operation.

pub fn compose(fns: Vec<Box<dyn Fn(i64) -> i64>>) -> Box<dyn Fn(i64) -> i64>

Fold a list of boxed closures into a single boxed closure applying them left to right: compose([f, g])(x) == g(f(x)). An empty list composes to the identity function. Box<dyn Fn(..) -> ..> is how you store a closure in a struct field or return one from a function, and it is by far the most common trait object in real code — every closure has its own unnameable anonymous type, so a trait object is the only way to put two of them in one Vec.

pub fn pipeline(ops: Vec<(String, i64)>, start: i64) -> i64

Build the operations and fold start through them in order.

Errors you may meet, and what they mean

  • E0277the size for values of typedyn Opcannot be known at compilation time. You wrote a DST where a Sized type was required.
  • E0782expected a type, found a trait. Writing Box<Op> instead of Box<dyn Op> was merely a warning in edition 2015; since edition 2021 it is a hard error. The dyn keyword is not optional.
  • E0746return type cannot have an unboxed trait object. You wrote -> dyn Op. Return Box<dyn Op> or impl Op.
  • E0038 — the trait is not dyn compatible. The next item is entirely about that one.

Remember the grade is compile + tests + clippy -D warnings.