Checkpoint: Traits & Generics
This checkpoint consolidates everything from the traits-and-generics chapter — trait objects, associated types, derive macros, operator overloading, Default / Clone , testing, file I/O, and command-line arguments — into one build challenge you write and run yourself.
Learn Checkpoint: Traits & Generics in our free Rust course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Rust course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Work through the recap, complete the multi-step shapes program, reveal the full solution to compare, and test your understanding with the checkpoint quiz.
What This Checkpoint Covers
📚 Chapter Recap
🛠️ Build Challenge: A Shapes Program
Put the chapter to work. You'll define a Shape trait with an area() method, implement it for several #[derive(Debug)] structs, store them in a , and sort them by area. Start with the smaller starter below, fill the two blanks, then grow it toward the full solution.
Now extend it: add a Rectangle and a Triangle , give Shape a name() method, sort the shapes by area with sort_by , print a running total, and report the largest. When you're ready, reveal the full solution to compare.
A complete, runnable solution with three shapes, sorting by area, a total, and the largest shape:
📝 Checkpoint Quiz
Answer each in your head, then click to reveal. If any trips you up, revisit the linked idea before moving on to the capstone.
Because a has no fixed size known at compile time — different implementors have different sizes. A pointer has a known size, so the trait object is stored behind , Box , Rc , etc. (it's a fat pointer: data pointer + vtable pointer).
Static dispatch (generics, ) resolves method calls at compile time and generates a specialized copy per type — fast, often inlined. Dynamic dispatch (trait objects, ) looks up the method at runtime through a vtable — one copy of the code, slightly slower, but able to mix types.
Copy requires every field to be Copy , so a struct with a String or Vec field can't be Copy . Derive Clone instead and call .clone() explicitly. (And Copy always requires Clone too.)
+ uses std::ops::Add ; a + b compiles to a.add(b) . Its associated Output type names what the operation produces — often Self , but it can differ (e.g. multiplying a vector by an f64 still yields the vector type).
Add #[should_panic(expected = "...")] above a #[test] to require a panic. To use ? inside a test, give the test a return type of and end with Ok(()) .
Index 0 is the program's own name, so real arguments start at index 1. Convert an argument String to a number with parse and a type annotation, handling the Result — e.g. .
Practice quiz
Why must a trait object always sit behind a pointer like &dyn or Box<dyn>?
- Because traits cannot be stored in variables
- Because the compiler forbids naming a trait directly
- Because dyn Trait has no size known at compile time, but a pointer does
- Because trait objects are always heap-only by design
Answer: Because dyn Trait has no size known at compile time, but a pointer does. A dyn Trait is unsized — different implementors have different sizes — so it must live behind a pointer (&, Box, Rc) with a known size; it becomes a fat pointer (data + vtable).
What is the difference between static dispatch and dynamic dispatch?
- Static dispatch (generics) resolves calls at compile time; dynamic dispatch (dyn) looks up methods at runtime via a vtable
- Static dispatch is always slower than dynamic dispatch
- They are two names for the same thing
- Dynamic dispatch generates a specialized copy per type
Answer: Static dispatch (generics) resolves calls at compile time; dynamic dispatch (dyn) looks up methods at runtime via a vtable. Generics resolve at compile time and specialize per type (often inlined). Trait objects use a runtime vtable lookup — one copy of the code, slightly slower, but able to mix types.
Why can't you always derive Copy for a struct, and what do you derive instead?
- Copy only works on enums; use Debug instead
- Copy requires a lifetime; use Default instead
- You can always derive Copy
- Copy needs every field to be Copy; derive Clone instead for types with String or Vec fields
Answer: Copy needs every field to be Copy; derive Clone instead for types with String or Vec fields. Copy requires every field to be Copy, so a struct holding a String or Vec can't be Copy. Derive Clone and call .clone() explicitly instead (and Copy always requires Clone too).
Which trait does the + operator use, and what names the result type?
- std::ops::Plus, with a Result type
- std::ops::Add, with an associated Output type
- std::cmp::Add, with a Self type
- std::ops::Sum, with a Total type
Answer: std::ops::Add, with an associated Output type. a + b compiles to a.add(b) via std::ops::Add; its associated Output type names what the operation produces — often Self, but it can differ.
Why does the shapes solution use sort_by with partial_cmp instead of sort?
- Areas are f64, and floats are PartialOrd but not Ord (because of NaN), so sort is unavailable
- sort is deprecated in Rust
- sort_by is always faster
- partial_cmp sorts in descending order automatically
Answer: Areas are f64, and floats are PartialOrd but not Ord (because of NaN), so sort is unavailable. f64 implements PartialOrd but not Ord because NaN is unordered, so the plain sort (which needs Ord) is not available. sort_by with partial_cmp().unwrap() is the standard pattern.
When should you reach for a Vec<Box<dyn Shape>> rather than a generic Vec<T>?
- When every element is the same known type
- When you want maximum inlining and speed
- When you need to store several different concrete types together behind one interface
- Never; generics can always hold mixed types
Answer: When you need to store several different concrete types together behind one interface. A generic Vec<T> holds one type at a time. Use Vec<Box<dyn Trait>> when you need a heterogeneous collection of different concrete types sharing one interface.
What does '#[derive(Debug)]' give you?
- Automatic equality comparison
- An auto-generated formatting implementation usable with the {:?} placeholder
- Thread-safety guarantees
- An automatic Drop implementation
Answer: An auto-generated formatting implementation usable with the {:?} placeholder. #[derive(Debug)] auto-writes a Debug implementation so the type can be printed with the {:?} (and {:#?}) format specifiers — invaluable for debugging and tests.
What is an associated type, such as 'type Item;' in Iterator?
- A second generic parameter passed at every call
- A runtime value attached to the trait
- A lifetime parameter
- A type slot that each implementor fills in once for the trait
Answer: A type slot that each implementor fills in once for the trait. An associated type is a placeholder the implementor fills in once (like Iterator's Item), as opposed to a generic parameter chosen anew at each use site.
How do you write a test that requires the code to panic?
Annotate the test with #[should_panic(expected = "...")] so it passes only if the code panics with a matching message.
In env::args(), what is at index 0, and where do real arguments start?
- Index 0 is the first real argument; arguments start at 0
- Index 0 is always empty; arguments start at index 2
- Index 0 is the program's own name; real arguments start at index 1
- There is no index 0
Answer: Index 0 is the program's own name; real arguments start at index 1. env::args() yields the program's own name at index 0, so user-supplied arguments begin at index 1.