Checkpoint: Traits & Generics

Reviewed & published by Brayan K

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.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, 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 Vec<Box<dyn Shape>>, and sort them by area. Start with the smaller starter below, fill the two blanks, then grow it toward the full solution.

// 🎯 BUILD CHALLENGE (starter) — fill the blanks, then extend it (see below).
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

#[derive(Debug)]
struct Square { side: f64 }
#[derive(Debug)]
struct Circle { radius: f64 }

impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.____ }     // 👉 a square's area
    fn name(&self) -> &str { "Square" }
}
impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }
    fn name(&self) -> &str { "Circle" }
}

fn main() {
    // 👉 a heterogeneous list needs boxed trait objects: Box<dyn Shape>
    let shapes: Vec<Box<dyn ____>> = vec![
        Box::new(Square { side: 2.0 }),
        Box::new(Circle { radius: 1.0 }),
    ];

    for s in &shapes {
        println!("{} area = {:.2}", s.name(), s.area());
    }
}

// ✅ Expected output:
//    Square area = 4.00
//    Circle area = 3.14

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 dyn Trait 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, <T: Trait>) resolves method calls at compile time and generates a specialized copy per type — fast, often inlined. Dynamic dispatch (trait objects, dyn Trait) 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 Result<(), E> 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. let n: i64 = arg.parse().unwrap_or(0);.

Pro Tips

🎉 Checkpoint Complete!

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?

  • Add #[panic] above the function
  • Add #[should_panic(expected = "...")] above the #[test]
  • Return Err from the test
  • Wrap the body in catch_unwind

Answer: Add #[should_panic(expected = "...")] above the #[test]. 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.

Continue this course

Frequently asked questions

What should I be able to do after this checkpoint?

You should be comfortable defining a trait and implementing it for several types, choosing between generics (static dispatch) and trait objects (dynamic dispatch), and storing mixed types in a Vec<Box<dyn Trait>>. You should also be able to derive common traits like Debug, Clone, PartialEq, and the ordering set; overload operators through std::ops; supply Default values; write tests with #[test] and cargo test; read and write files with std::fs; and read command-line arguments and environment variables with std::env. The build challenge below exercises several of these at once.

Why does the solution use sort_by with partial_cmp instead of sort?

The shapes are compared by area, which is an f64. Floating-point numbers implement PartialOrd but not Ord, because the special value NaN is not ordered relative to anything, so the plain sort method (which requires Ord) is not available. sort_by lets you pass a closure that returns an Ordering, and partial_cmp produces an Option<Ordering> that we unwrap, which is safe here because real areas are never NaN. This is the standard pattern for sorting floats.

When would I pick a trait object over a generic here?

Use a trait object, as the shapes Vec does, when you need to store or process values of several different concrete types together behind one interface — a generic Vec<T> can only hold one type at a time. Use a generic function or struct when each call site works with a single known type and you want the speed of static dispatch and inlining. The shapes program needs a mixed collection, so Vec<Box<dyn Shape>> is the right tool; a function that only ever sorts one shape type could be generic instead.

How do I run the build challenge myself?

Put the code in a file such as main.rs inside a Cargo project and run cargo run, or paste it straight into the Rust Playground and press Run. The starter is a smaller version with two blanks to fill; once that compiles and prints the two areas, extend it toward the full solution by adding more shapes, deriving Debug, and sorting by area. The expected output is shown beneath each block so you can check your work exactly.