When boilerplate steals your time
You define a struct to hold a rectangle's dimensions. You try to print it to the console. The compiler rejects you. You try to check if two rectangles are the same size. The compiler rejects you again. You try to clone the rectangle so you can modify a copy without touching the original. The compiler rejects you a third time.
Rust's type system demands that you prove your data can be printed, compared, or copied before you do those things. This keeps the language safe and predictable. It also means you have to write the same repetitive code for every struct you create. Writing manual implementations for basic operations is tedious and error-prone. You spend time typing boilerplate instead of solving the actual problem.
Rust provides a shortcut. The #[derive] attribute tells the compiler to generate the trait implementations for you automatically. You list the traits you need, and the compiler writes the code based on your struct's fields. It's not magic. It's a macro that expands at compile time, inspecting your data and producing the necessary impl blocks.
What derive actually does
#[derive] is a built-in macro. When the compiler sees #[derive(Debug, Clone)] above a struct, it runs a code generator. The generator looks at every field in the struct. It checks if each field implements the requested traits. If every field supports the trait, the generator writes an implementation for the struct that delegates to the fields.
The generated code is recursive. If your struct contains another struct, the derive macro checks the inner struct. If the inner struct also derives the trait, the chain holds. If any field in the chain is missing the trait, the derivation fails. The compiler enforces this strictly. You cannot derive a trait on a type unless all its parts support that trait.
This design gives you two benefits. You get correct implementations without typing them. You also get a guarantee that your type's behavior matches its contents. If a field changes and stops implementing a trait, the compiler catches the breakage immediately.
Minimal example: Debug
The most common derive is Debug. This trait enables formatted printing with the {:?} specifier. It's essential for logging and debugging. Without it, you can't print the struct at all.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let origin = Point { x: 0, y: 0 };
// Debug trait enables the {:?} format specifier.
// The compiler generated an impl that prints field names and values.
println!("Origin is {:?}", origin);
}
The output looks like Origin is Point { x: 0, y: 0 }. The format includes the struct name and every field. This is verbose by design. It helps you identify exactly which struct you're looking at when debugging complex nested data.
If you remove #[derive(Debug)], the compiler rejects the println! macro with E0277 (the trait Debug is not implemented for Point). The error message tells you exactly what's missing and suggests adding the derive attribute.
Trust the compiler here. If you can't print your data, you can't debug it. Add #[derive(Debug)] to every struct as a habit.
How the compiler fills in the blanks
When you derive a trait, the compiler generates code that mirrors the structure of your type. For a struct, it generates code that operates on each field in order. For an enum, it generates code that handles each variant.
The generated implementation is usually straightforward. Clone copies each field. PartialEq compares each field. Hash hashes each field. The compiler handles the recursion. If a field is a Vec, the derive calls Vec's implementation. If a field is a String, it calls String's implementation.
This field-by-field approach means the derived behavior is consistent with the data layout. Two structs are equal if and only if all their fields are equal. Two structs have the same hash if and only if all their fields hash to the same value. This consistency is critical for correctness, especially when using types as keys in maps or sets.
Realistic example: The entity chain
Real applications often need a chain of traits. You might want to clone a value, compare it for equality, and use it as a key in a hash map. This requires Clone, PartialEq, Eq, and Hash.
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Player {
id: u64,
name: String,
score: i32,
}
fn main() {
let p1 = Player {
id: 1,
name: "Alice".to_string(),
score: 100,
};
// Clone creates a deep copy of the Player.
// The String inside is also cloned, allocating new heap memory.
let p2 = p1.clone();
// PartialEq and Eq allow comparison.
// Eq implies reflexivity, which derive guarantees for these types.
assert_eq!(p1, p2);
// Hash allows using Player as a key in a HashMap.
// The hash is computed from id, name, and score.
let mut leaderboard = HashMap::new();
leaderboard.insert(p1, "Gold");
// Lookup works because p2 hashes to the same value as p1.
println!("Rank: {:?}", leaderboard.get(&p2));
}
The derive list includes Eq alongside PartialEq. Eq is a marker trait that asserts the equality relation is reflexive, symmetric, and transitive. PartialEq alone allows for weird cases like NaN != NaN. Eq promises that x == x is always true. Deriving Eq is safe here because u64, String, and i32 all implement Eq.
The Hash derive requires Eq. The compiler enforces this dependency. You cannot derive Hash on a type that doesn't derive Eq. This prevents a common bug where two equal values hash differently, which breaks hash maps.
Convention aside: The community often groups derives in a standard order. Clone, Debug, PartialEq, Eq, Hash is a common pattern. The order in the attribute doesn't matter to the compiler, but alphabetical or logical grouping helps readers scan the code quickly.
The float trap
Derive is powerful, but it's not infallible. It generates code based on the traits your fields implement. If a field has subtle behavior, the derived code inherits that behavior. This becomes dangerous with floating-point numbers.
f32 and f64 implement PartialEq, but they do not implement Eq. The reason is NaN (Not a Number). In IEEE 754 floating-point arithmetic, NaN != NaN. This violates the reflexivity requirement of Eq. If you try to derive Eq on a struct containing a float, the compiler rejects you.
#[derive(Debug, PartialEq, Eq)]
struct Stats {
accuracy: f32,
damage: i32,
}
This code fails with E0277 (the trait Eq is not implemented for f32). The compiler stops you from making a false promise. Your struct cannot be Eq because one of its fields breaks the contract.
If you need to store floats in a hash map or set, you have options. You can round the floats to a fixed precision before comparison. You can wrap the float in a newtype that implements Eq and Hash with custom logic. You can avoid floats entirely by using integers scaled to a fixed point. Derive won't save you here. You need a manual implementation that handles the math correctly.
Counter-intuitive but true: deriving PartialEq on a struct with floats can break your hash map even if you don't derive Eq. If two structs compare equal but hash differently, the map fails to find keys. Always ensure your equality and hash implementations are consistent.
Clone is a deep copy
#[derive(Clone)] generates a deep copy. Every field is cloned recursively. If your struct contains a String, the derived clone allocates new heap memory and copies the bytes. If it contains a Vec, it allocates a new vector and copies the elements.
This behavior is correct and safe. It ensures that the cloned value is independent of the original. Modifying the clone doesn't affect the source. However, deep copying can be expensive. Cloning a large struct in a tight loop can destroy performance.
#[derive(Clone)]
struct BigData {
buffer: Vec<u8>,
metadata: String,
}
fn process(data: BigData) {
// This clones the entire buffer and metadata.
// If buffer is 1MB, this allocates 1MB of memory.
let copy = data.clone();
// Work with copy...
}
If you need to share data without copying, use reference counting. Rc<T> for single-threaded code or Arc<T> for multi-threaded code. Wrapping the data in Rc changes the clone behavior. Rc::clone bumps a counter instead of copying the data. This is a shallow copy.
Convention aside: When cloning an Rc, write Rc::clone(&data) instead of data.clone(). Both compile and both work. The explicit form signals to readers that you're cloning the reference, not the underlying data. It avoids the mental jump of wondering if clone() is doing a deep copy.
If your struct contains types that don't implement Clone, like Mutex or file handles, you can't derive Clone. The compiler rejects the derive with E0277. You have to decide how to handle the non-cloneable field. Wrap it in Arc, remove it from the struct, or implement Clone manually with custom logic.
Pitfalls and compiler errors
Derive simplifies code, but it introduces specific failure modes. Understanding these helps you debug quickly.
E0277: Trait not implemented. This is the most common error. It means a field in your struct doesn't implement the trait you're trying to derive. Check the field types. Add derives to nested structs. Or implement the trait manually for the problematic field.
E0599: No function or associated item found. This happens when you try to use a method from a trait you forgot to derive. If you call .clone() on a struct without Clone, the compiler yells. The error message usually suggests adding the derive.
E0369: Binary operation not supported. This appears when you use == on a struct without PartialEq. The compiler tells you the trait is missing. Add #[derive(PartialEq)].
Hash consistency violations. The compiler can't check if your hash implementation is consistent with equality. If you derive both, the generated code is consistent. If you mix derived and manual implementations, you risk breaking the contract. If a == b, then a.hash() == b.hash() must hold. If you change the equality logic manually but keep the derived hash, you break this rule. Hash maps will misbehave in subtle ways.
Default derive limitations. #[derive(Default)] generates a default() method that sets every field to its default value. u32 becomes 0. String becomes "". Vec becomes []. This works only if every field implements Default. If you have a field that needs a specific default, like a multiplier that should be 1.0 instead of 0.0, derive won't work. You need a manual impl Default.
Don't fight the compiler here. If derive doesn't match your semantics, write the impl. The extra lines of code are worth the correctness.
Decision matrix
Use #[derive(Debug)] when you need logging output or debugging visibility and don't require a custom format. Use #[derive(Clone)] when you need to copy values and the cost of deep copying is acceptable for your performance profile. Use #[derive(PartialEq, Eq)] when you need to compare values for equality and store them in sets or maps, ensuring all fields support strict equality. Use #[derive(Hash)] when you need to use the type as a key in a HashMap or HashSet, and the type already implements Eq. Use #[derive(Default)] when the zero-value or empty-value of every field makes sense as the default for the struct. Reach for a manual implementation when you need custom logic, such as ignoring certain fields in comparison, handling floating-point numbers safely, optimizing clone performance with reference counting, or providing non-zero defaults.
Derive saves time, but understand what it generates. The compiler writes the code, but you own the semantics. If the generated behavior doesn't match your intent, write the impl yourself.