Derive Macros and Common Traits
#[derive(Debug, Clone, PartialEq)] is the kind of line you write without thinking about it, until a struct holds a field the derived implementation handles wrong: a secret that shouldn't print, a float that breaks equality, a cache field that shouldn't affect comparison. Each derive macro applies one mechanical rule, field by field. This tutorial covers what that rule actually is for the traits you'll derive constantly, and the specific situations where you need to write the impl by hand instead.
Debug: Almost Always Safe, Except for Secrets
#[derive(Debug)] formats a value by printing its type name and every field's Debug output. It's close to always safe to derive, and you should derive it on nearly everything, {:?} formatting is how you'll inspect values in tests and println! debugging for the entire lifetime of the type.
The one case to hand-write Debug instead: a struct holding something that shouldn't end up in a log line.
struct Credentials {
username: String,
password: String,
}
impl std::fmt::Debug for Credentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Credentials")
.field("username", &self.username)
.field("password", &"[redacted]")
.finish()
}
}
A derived Debug on Credentials would happily print the plaintext password the first time someone logs the struct or it ends up in a panic message. Any type holding a secret, a token, a password, an API key, deserves a hand-written Debug that redacts it, derived or not.
Clone vs Copy: Deep Field-by-Field, or Bitwise
#[derive(Clone)] calls .clone() on every field and assembles a new value from the results. For a struct wrapping an Rc<T> or Arc<T>, that's cheap, cloning the pointer just bumps a refcount. For a struct wrapping a Vec<T> or String, it's a real deep copy of the underlying buffer. The derive macro doesn't distinguish between the two, "clone" can mean very different costs depending on what's inside.
#[derive(Copy)] is far more restrictive: it's only valid when every field is itself Copy, no String, Vec, Box, or anything else that owns a heap allocation. Copy also requires Clone to be derived alongside it.
#[derive(Clone, Copy)]
struct Point { x: f64, y: f64 } // fine: f64 is Copy
#[derive(Clone)] // Copy would not compile here
struct Config { name: String, retries: u32 }
If a type is small and every field is a plain number or another Copy type, derive Copy too, it makes the type behave like an i32, copied implicitly instead of requiring an explicit .clone() everywhere. The compiler refuses to derive Copy if any field doesn't qualify, so there's no risk of getting this wrong silently.
PartialEq/Eq: The NaN Problem
#[derive(PartialEq)] compares two values by comparing every field with ==. Eq is a marker trait layered on top: it asserts that equality is reflexive, a == a is always true. This sounds automatic, until a field is an f64 or f32.
#[derive(PartialEq)]
struct Measurement { value: f64 }
// #[derive(Eq)] on the line above would fail to compile:
// f64 doesn't implement Eq, because NaN != NaN
Floats implement PartialEq but deliberately not Eq, by the IEEE 754 spec, NaN == NaN is false. That single broken case (a == a failing) means no f64-containing type can honestly implement Eq, and the compiler enforces it: deriving Eq on a struct with a float field doesn't compile. If you need Eq (most commonly because a type is going into a HashMap key or HashSet), use an integer-based representation instead, like storing cents instead of a dollar f64, rather than working around the restriction.
Hash: Must Agree With PartialEq, Always
#[derive(Hash)] hashes a value by hashing every field and combining the results. The invariant it depends on, and that you must preserve if you ever hand-write PartialEq instead of deriving it, is: if a == b, then hash(a) == hash(b).
This goes wrong when a struct has a field that should be ignored for equality, like an internal cache or a last-accessed timestamp, but Hash is still derived against every field including it.
#[derive(Hash)]
struct User {
id: u64,
name: String,
last_login: std::time::SystemTime, // shouldn't affect equality
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id // only `id` defines equality
}
}
This compiles, but it's broken: two Users with the same id and different last_login are == but hash differently. Insert both into a HashSet<User> and you can end up with two "equal" entries coexisting, because the set never realized they collide. Whenever you hand-write PartialEq, hand-write Hash to match it field-for-field, don't derive one and write the other by hand.
impl std::hash::Hash for User {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state); // hash exactly the fields eq() compares
}
}
PartialOrd/Ord: Field Order Is Comparison Priority
Derived ordering compares fields lexicographically, in the order they're declared in the struct. The first field is the primary sort key, the second only breaks ties on the first, and so on.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
let mut versions = vec![
Version { major: 1, minor: 4, patch: 0 },
Version { major: 1, minor: 2, patch: 9 },
Version { major: 2, minor: 0, patch: 0 },
];
versions.sort();
// [1.2.9, 1.4.0, 2.0.0] — sorted by major, then minor, then patch
This is exactly the comparison you want for semantic versioning, because major, minor, patch are declared in priority order. Reordering the fields silently changes sort behavior, swapping minor and major in the struct definition would sort by minor version first, with no compiler error to flag it. Field order is part of the type's public behavior the moment you derive Ord, treat reordering fields as a breaking change, not a cosmetic one.
Default: Per-Field for Structs, Explicit for Enums
#[derive(Default)] on a struct sets every field to its own Default::default(). On an enum, there's no field-by-field rule to fall back on, you have to mark which variant is the default explicitly:
#[derive(Default)]
enum LogLevel {
Debug,
#[default]
Info,
Warn,
Error,
}
Without #[default] on exactly one variant, #[derive(Default)] on an enum simply doesn't compile, there's no sensible default to infer from an enum's shape the way there is from a struct's fields.
Key Takeaways
- Derive
Debugon nearly everything, except types holding secrets, tokens, or passwords, hand-write those to redact the sensitive field. Cloneis always a deep, field-by-field clone; the cost depends entirely on what's inside (anRcclone is cheap, aVecclone isn't).Copyis only derivable when every field is itselfCopy.- Deriving
Eqfails to compile on any type containing a float,NaN != NaNbreaks the reflexivityEqrequires. Use an integer representation for values you need asHashMap/HashSetkeys. - If you hand-write
PartialEq, hand-writeHashto match it exactly. A derivedHashpaired with a hand-writtenPartialEqthat ignores a field silently breaksHashSet/HashMapinvariants. - Derived
Ord/PartialOrdcompares fields in declaration order, the first field is the primary sort key. Reordering fields changes sort behavior with no compiler warning. #[derive(Default)]works automatically on structs (default per field), but enums need#[default]on exactly one variant.