Advanced Traits

Course 3 · Ch 1
Advanced Traits
Four trait features that unlock more expressive APIs, and one question they all answer differently

Course 2 Chapter 2 covered trait basics — definitions, default implementations, trait bounds. This chapter goes deeper into four features that unlock more expressive APIs: associated types, trait objects, operator overloading, and supertraits.

Associated Types

A trait can declare a type placeholder that's part of the trait's own contract, filled in by each implementor — not a separate generic parameter:

trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } struct Counter { count: u32 } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<u32> { /* ... */ } }

Why not a generic trait Iterator<T> instead? A type should have exactly one Item type per implementation — associated types express "one specific type per impl"; a generic trait parameter would instead allow the same type to implement Iterator<u32> and Iterator<String> simultaneously, which isn't the intended shape here at all.

Trait Objects (dyn Trait)

Course 2's generics achieve polymorphism via monomorphization — compile-time, zero-cost, but exactly one concrete type per instantiation. dyn Trait is the runtime alternative: a trait object stores a pointer to the data plus a pointer to a vtable (a table of function pointers for that trait's methods):

let items: Vec<Box<dyn Summary>> = vec![ Box::new(Article { /* ... */ }), Box::new(Tweet { /* ... */ }), ]; for item in &items { println!("{}", item.summarize()); }

This is something monomorphized generics genuinely cannot do: a Vec<T> can only ever hold one concrete T, but Vec<Box<dyn Summary>> holds a heterogeneous collection — different concrete types, unified only by implementing the same trait.

Trait Objects vs. Generics: The Real Trade-off

The standard vocabulary: generics use static dispatch (resolved at compile time, zero runtime cost); dyn Trait uses dynamic dispatch (resolved at runtime via the vtable, a small real cost).

Generics (Static Dispatch)dyn Trait (Dynamic Dispatch)
ResolvedAt compile time (monomorphization)At runtime (vtable lookup)
Runtime costZeroSmall — one indirect call per invocation
Can mix concrete types?No — one T per instantiationYes — a genuinely heterogeneous collection

Operator Overloading

The std::ops traits (Add, Sub, Mul, ...) each declare an associated type — Add itself has type Output — directly putting this chapter's first feature to practical use:

use std::ops::Add; impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } } } let p3 = p1 + p2; // calls Point's Add::add

Supertraits

A trait can require another trait as a prerequisite, letting a default implementation safely rely on the supertrait's methods:

trait DisplaySummary: std::fmt::Display { fn display_summary(&self) -> String { format!("({})", self) // {} formatting requires Display — guaranteed by the supertrait bound } }

Any type implementing DisplaySummary must also implement Display — the compiler enforces this at the impl site.

Associated Types

type Item; — one specific type per implementation, part of the trait's own contract.

dyn Trait

Dynamic dispatch via a vtable — enables heterogeneous collections generics can't express.

Operator Overloading

Implementing std::ops traits (Add, Sub, ...) to customize +, -, and similar syntax.

Supertraits

Trait: OtherTrait — requires OtherTrait, letting default methods rely on it safely.

One Question, Four Answers
All four features answer variations of "how much do I know about a type at compile time, versus how much flexibility do I need at runtime?" Associated types narrow a trait to one specific type per impl. dyn Trait defers the concrete type to runtime entirely. Operator overloading customizes syntax for a known concrete type. Supertraits compose known guarantees together. Different answers to the same underlying question.
Not Every Trait Can Become a Trait Object
dyn Trait requires the trait to be object safe. A trait with a method returning Self (not Box<Self> or similar), or with generic methods, generally cannot be turned into a trait object — the vtable has no way to represent "return the exact concrete Self type" without knowing what Self actually is. This is a real, sometimes-surprising compiler restriction worth knowing by name, even without memorizing every specific rule.

Coding Challenges

Challenge 1

Define a trait Container with an associated type Item and a method get(&self, index: usize) -> Option<&Self::Item>. Implement it for a struct StringBag wrapping a Vec, with type Item = String.

📄 View solution
Challenge 2

Write a function print_all(items: &[Box]) that prints every item in a heterogeneous slice, then call it with a mix of an i32, a String, and an f64 all boxed as dyn Display.

📄 View solution
Challenge 3

Implement std::ops::Sub for a Point struct so that p1 - p2 works, following this chapter's Add example as a template. Then explain, in your own words, why Add's associated type Output isn't always the same type as Self, using a hypothetical example where it wouldn't be.

📄 View solution

Chapter 1 Quick Reference

  • type Item; — an associated type; one specific type per trait implementation, not a generic parameter
  • dyn Trait — a trait object; dynamic dispatch via a vtable, enables heterogeneous collections
  • Static dispatch (generics) — zero runtime cost, one concrete type per instantiation
  • Dynamic dispatch (dyn Trait) — small runtime cost, mixes concrete types behind one trait
  • impl Add for Type { type Output = ...; fn add(...) } — operator overloading via std::ops
  • trait A: B { ... } — a supertrait; implementing A requires also implementing B
  • Not every trait is object-safe — methods returning Self or using generics generally block dyn Trait use
  • Next chapter: Unsafe Rust — raw pointers, unsafe blocks, when/why to opt out of the borrow checker, basic FFI