Skip to content

← Trait Objects and Dispatch step 7 of 10

Medium Primitives

Dispatch through Rc: Rc<dyn Trait> and shared behaviour

Box<dyn Trait> gives you one owner of an erased type. But an observer registry — the shape behind every event bus, every plugin table, every “call these five things when something happens” — usually wants the same handler registered in several places. One owner is not enough. You want shared ownership of an erased type, and that is Rc<dyn Trait>.

This is Track 11 and Track 12 meeting: reference counting plus dynamic dispatch, in the one composition that appears constantly in real code.

What Rc<dyn Handler> actually is

Rc<T> is a thin pointer to a heap box holding { strong, weak, T }. Rc<dyn Handler> is a fat pointer to that box: a data pointer plus a vtable pointer. So calling h.handle(e) costs two hops — one through the Rc to the box, one through the vtable to the function. That is the double-indirection point from the smart-pointer track, now in a dispatch setting, and it is why an Rc<dyn _> in a hot inner loop is one of the slowest shapes you can write.

Cloning it is cheap and does not clone the handler: it bumps a counter and copies two words. That is the whole point.

What to write

A Handler trait and five implementors are given. Build the registry — Vec<Rc<dyn Handler>> — with one Rc instance per distinct name, and make repeated names share that instance via Rc::clone.

pub fn broadcast(handlers: Vec<String>, events: Vec<i64>) -> Vec<i64>

For each event, in order, return the sum of what every registry entry returns for it. A duplicated handler contributes twice — it is in the registry twice, even though there is only one instance of it in memory. An empty registry sums to 0.

The names are "double" (e * 2), "negate" (-e), "addten" (e + 10) and "square" (e * e). Any other name maps to the Ignore handler, which contributes 0.

pub fn share_counts(handlers: Vec<String>) -> Vec<usize>

Build the same registry, then return Rc::strong_count for the first occurrence of each distinct name, in first-appearance order.

Read that carefully, because it pins down a real subtlety. If you build the registry with a lookup table (HashMap<&str, Rc<dyn Handler>>) and the table is still alive when you count, every count is one higher, because the table holds a strong reference too. Drop the lookup table before counting. Then strong_count is exactly the number of times that name appeared — ["double", "double", "negate"] gives [2, 1].

That is not a trick question. It is the thing that makes Rc counts confusing in real programs: the count answers “how many strong handles exist right now“, and caches, closures and temporaries all hold handles.

The error the starter hands you

E0782expected a type, found a trait. Writing Rc<Handler> instead of Rc<dyn Handler> was a lint in edition 2015 and is a hard error from edition 2021 onwards. The compiler is not being pedantic: Handler alone is ambiguous between “the trait, as a bound” and “some value implementing it”, and the two compile to completely different code.

Lints in the neighbourhood

  • redundant_allocationRc<Box<dyn Handler>> is two heap boxes and two indirections where one would do. Rc<dyn Handler> already handles the unsized type.
  • borrowed_box&Box<dyn Handler> should be &dyn Handler.
  • clone_on_ref_ptr (restriction, off by default) — argues for writing Rc::clone(&x) rather than x.clone(), so that a reader can see at a glance that this is a refcount bump and not a deep copy. It is off by default and the community is split, but the argument is worth knowing.

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

Loading visualization…