Skip to content

← Trait Objects and Dispatch step 2 of 10

Hard Primitives

Keep concrete methods while enabling trait objects

Make the supplied Render trait usable through Box<dyn Render>. Add where Self: Sized to the tagged and duplicated methods, keeping both methods available on Dot and Word. Do not delete methods, change their behavior or require Sized on the trait itself.

The starter fails with E0038 because tagged has a type parameter and duplicated returns Self. label can remain callable through the trait object.

pub fn dyn_labels(specs: Vec<String>) -> Vec<String>
pub fn concrete_report(kind: String, payload: String) -> Vec<String>

Preserve the supplied parsing and output

dyn_labels processes each spec independently, preserving order and duplicates:

  • Split at the first colon. If the part before it is exactly "dot", parse the remainder as usize and return that many dots. A failed parse means zero dots, or the empty string.
  • For any other prefix before a colon, uppercase the entire remainder.
  • With no colon, uppercase the entire spec.

There is no trimming. For example, "dot:+03" gives "...", "dot: 3" gives "", "word:a:b" gives "A:B", and "DOT:hi" gives "HI". Dot parsing accepts decimal ASCII digits, an optional leading plus and leading zeros; negative, empty, malformed or out-of-usize-range counts fail. Empty specs input produces an empty vector.

concrete_report returns two strings:

Kind First string Second string
Exactly "dot" One dot per UTF-8 byte of payload, then #, then the decimal byte count. Twice that many dots.
Any other string Uppercased payload followed by #cap. Payload concatenated with itself, then uppercased.

For kind dot and payload "é", the result is ["..#2", "...."]. For kind word and payload "hi", it is ["HI#cap", "HIHI"]. The dot branch uses payload's byte length, not a number parsed from its contents.

Uppercasing is Unicode-aware and not locale-specific; it can expand text, as with ß becoming SS. Other characters, including whitespace, are preserved unless uppercasing changes them. Unicode tables can vary with Rust versions. Dot output allocates one byte per dot; the supplied implementation assumes the requested output fits available memory and the doubled byte count fits usize.

What the method bound changes

Dyn compatibility, formerly called object safety, determines whether a trait can be used as the base of a trait object. A trait object provides an erased interface to a concrete implementation. Calls through that interface use dispatch information; its exact vtable layout is not a stable API contract.

A method with where Self: Sized is excluded from that interface. The erased dyn Render type is unsized, so this exercise's objects can call label but cannot call tagged or duplicated through Render. Dot and Word are sized and retain all three methods. Put the restriction on the two methods, not on Render itself.

Here is a complete example of the same pattern:

trait Describe {
    fn label(&self) -> String;
    fn repeat(&self, count: usize) -> String where Self: Sized {
        self.label().repeat(count)
    }
}

struct Mark;

impl Describe for Mark {
    fn label(&self) -> String { "x".to_string() }
}

fn main() {
    assert_eq!(Mark.repeat(3), "xxx");
    let object: Box<dyn Describe> = Box::new(Mark);
    assert_eq!(object.label(), "x");
}

Adding object.repeat(3) would fail because that method requires a sized implementor. The Box itself being sized does not make its dyn Describe referent sized.

Check the specific compatibility restriction

Type- or const-generic methods cannot be dispatched through an ordinary trait object; lifetime parameters are allowed. Returning Self also requires a concrete result type that the erased interface does not provide. Other restrictions concern supertraits, associated items, receiver types and opaque return types such as those from async methods.

Use the dyn compatibility reference for the detailed rules. A method-level Sized restriction addresses these two methods; it is not a universal repair for every incompatible trait.

Associated types are a different issue. This complete example specifies Iterator's Item type and uses the resulting trait object:

fn main() {
    let mut values: Box<dyn Iterator<Item = u32>> = Box::new(1..3);
    assert_eq!(values.next(), Some(1));
    assert_eq!(values.next(), Some(2));
    assert_eq!(values.next(), None);
}

Writing Box<dyn Iterator> here instead produces E0191 for the missing Item binding. That is distinct from the starter's E0038.

A different API could accept &dyn Display instead of a generic tagged argument, or return a boxed trait object instead of Self. Those choices change how callers use the methods. This exercise preserves the existing concrete API and opts its two methods out of dynamic dispatch.

Passing requires both report functions to keep working, compilation and Clippy with warnings treated as errors.