Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🦀 Tip of the day 26: Derive Macros, or How to Make the Compiler Write Your Boilerplate

You have a struct. You want to print it for debugging. You write the natural thing:

#![allow(unused)]
fn main() {
struct Player {
    name: String,
    score: u32,
}

let p = Player { name: String::from("Alice"), score: 42 };
println!("{:?}", p);  // ERROR: `Player` doesn't implement `Debug`
}

The compiler complains: Player does not implement the Debug trait. You can write that implementation yourself, but for a “print every field” version, the compiler will write it for you. Just ask:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Player {
    name: String,
    score: u32,
}

let p = Player { name: String::from("Alice"), score: 42 };
println!("{:?}", p);  // Player { name: "Alice", score: 42 }
}

#[derive(...)] is an attribute that asks the compiler to generate a standard implementation for one or more traits. Each name inside the parentheses is a trait. The compiler reads your struct and writes the impl for you.

The standard derives you will use most often:

  • Debug: enables {:?} formatting in println! and friends.
  • Clone: gives your type a .clone() method. It calls .clone() on each field in turn, so the semantics depend on what each field’s Clone impl does.
  • Copy: opts into copy-on-assignment (tip 19). Only legal when every field is also Copy.
  • PartialEq: enables == and != comparisons.
  • Eq: a marker trait paired with PartialEq, asserting equality is reflexive (x == x always holds). No methods, no runtime cost. Required alongside Hash for HashMap keys.
  • Default: gives you Type::default(), returning the default value (zero for numbers, empty for collections).
  • Hash: lets the type be a key in HashMap and HashSet.
  • PartialOrd: enables <, <=, >, >= operators. Can answer “less”, “equal”, “greater”, or “incomparable”, and that last case is why f32 and f64 only implement this one (NaN is incomparable with anything, including itself).
  • Ord: a total ordering: any two values compare to a definite answer. Required for sorting and for BTreeMap / BTreeSet keys. Ord is a sub-trait of PartialOrd and Eq, so the four typically travel together: #[derive(PartialEq, Eq, PartialOrd, Ord)].

You can stack them:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Default)]
struct Player {
    name: String,
    score: u32,
}
}

A note on what derive macros actually are. They are not the same machinery as the ! macros from tips 1 and 2. Those are declarative macros: pattern substitution on tokens.

#[derive(...)] is a procedural macro. Proc macros are real Rust code that runs at compile time, takes your syntax as input, and produces new syntax as output. They come in three flavors:

  • Function-like (my_macro!(...)): used the same way as declarative macros, but written as proc-macro code. Example: sqlx::query!(...).
  • Attribute (#[my_attr]): can rewrite the entire item it is attached to. Example: #[tokio::main].
  • Derive (#[derive(MyTrait)]): can only add new impl blocks. It never modifies the item itself, which is why derives feel safe to read.

The standard derives are built into the compiler. Custom ones come from external crates: serde::Serialize for JSON serialization, thiserror::Error for error types, and so on.

You do not need to know how to write any flavor to use them. You write the attribute, the compiler runs the macro, the impl appears.

A few things worth knowing about the standard derives (custom derives from crates follow similar conventions but can set their own rules):

  • Every field must implement the trait you are deriving. #[derive(Debug)] on a struct with a non-Debug field is a compile error.
  • Copy cannot be derived without Clone. Copy is a sub-trait of Clone, so the pair always travels together.
  • Derives apply to enums as well as structs. The standard ordering and equality derives compare variants by tag first, then by field.
  • For PartialOrd and Ord, the comparison is lexicographic in the order the fields are declared. If score should dominate name, declare score first.
  • To see what was generated by any derive (standard or custom), install cargo-expand and run cargo expand. It prints your code with all derives expanded inline.

The takeaway: #[derive(...)] asks the compiler to write a standard implementation of a trait for you. Most Rust structs have at least #[derive(Debug)], and many have Clone, PartialEq, Default alongside.

🔇 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Error)] fits on one line. Whether it should is a separate conversation.