Many common traits have an obvious, mechanical implementation that can be
generated automatically from a type's structure — comparing a struct
field-by-field for PartialEq, printing every field for Debug, cloning
every field for Clone. #[derive(...)] generates exactly that
mechanical implementation, saving the boilerplate of writing it by hand —
for example, #[derive(Debug, Clone, PartialEq)] on a Point { x: f64, y: f64 }
struct generates all three implementations at once.
A derive only works if every field's own type already implements the
trait being derived — deriving Clone for a struct containing a
non-Clone field is a compile error, since there'd be no way to generate
a working implementation. This is also the mechanism behind
serde's #[derive(Serialize, Deserialize)] — the same
generate-from-structure idea, extended by a third-party procedural macro
rather than the small fixed set of derives built into the compiler
itself, which is why the serialization
ecosystem is able to plug into ordinary struct/enum definitions with a
single attribute rather than requiring hand-written conversion code.
Basic usage example
#[derive(Debug, Clone, PartialEq)] // <- mechanically generates all three impls
struct Point { x: f64, y: f64 }
let a = Point { x: 1.0, y: 2.0 };
let b = a.clone();
println!("{:?} {}", b, a == b);
Restriction: a derive only works if every field's own type already
implements the trait being derived — a struct with a non-Clone field
cannot itself derive Clone.
Best practices & deeper information
Scenario
Implementing traits
#[derive(PartialEq)] covers a plain field-by-field comparison, but a
type with a field that shouldn't count toward equality — a cache, a
timestamp — needs a manual impl instead.
Why this way:#[derive(...)] always compares/prints/clones every
field — the moment equality (or Debug, or Clone) needs to mean
something other than "all fields, mechanically," a manual impl is the
only option; the
API Guidelines' C-COMMON-TRAITS
still recommends implementing the trait, just not necessarily by
deriving it.
Scenario
Testing
assert_eq! requires both PartialEq (to compare) and Debug (to print
the values in the failure message) — deriving both is what makes ordinary
equality assertions possible in a test.
#[derive(Debug, PartialEq)] // <- both required: PartialEq for `==`, Debug for the failure message
struct Order { id: u32, total_cents: u32 }
#[test]
fn totals_order_correctly() {
let order = Order { id: 1, total_cents: 1999 };
assert_eq!(order, Order { id: 1, total_cents: 1999 }); // needs PartialEq + Debug on Order
}
Why this way: without Debug the assertion doesn't compile at all
(not just on a runtime mismatch), since assert_eq! must be able to
print both sides on failure — the
Rust Book's testing chapter
notes both PartialEq and Debug are needed for assert_eq!.
Explanation
Embedded support: Full
The mechanism is unchanged under #![no_std] — every built-in derive
still generates the same mechanical, field-by-field implementation,
routed through core::fmt/core::cmp/core::clone rather than their
std re-exports, and still requires every field's own type to already
implement the trait being derived. What's worth deciding deliberately on
an embedded target is which traits to derive on which types, because
one of them has a cost that's easy to ignore on a hosted machine and hard
to ignore on a flash-constrained chip: #[derive(Debug)] generates
formatting code — matching every variant, writing every field name — that
occupies flash, and deriving it reflexively on every type in a large HAL
or driver crate can measurably inflate a release binary. See
#[derive(...)]'s embedded
explanation for the full code-size
argument and the #[cfg_attr(feature = "debug-impls", derive(Debug))]
pattern used to keep it optional; the choice that belongs here, on the
concept page, is when a type genuinely needs Debug/Clone/PartialEq
at all — a hot, frequently-instantiated register-snapshot type is a poor
candidate for a reflexive Debug derive, while a rarely-constructed
configuration struct usually isn't worth worrying about either way.
Basic usage example
#![no_std]
#[derive(Clone, Copy, PartialEq)] // <- routes through core::clone/core::cmp; Debug left out deliberately
pub struct SensorReading {
pub raw_adc: u16,
}
Best practices & deeper information
Scenario
Implementing traits
A register-snapshot type read many times per second is exactly the kind
of type where reflexively deriving Debug alongside Clone/PartialEq
is worth reconsidering, since only the latter two are needed for the
driver's own logic to compare and copy readings.
#![no_std]
#[derive(Clone, Copy, PartialEq)] // <- needed: the driver compares and copies readings internally
pub struct StatusRegister {
pub raw: u32,
}
// Debug omitted here on purpose — this type is read on every poll loop
// iteration, and its formatting code would occupy flash for a capability
// the release firmware never exercises.
Why this way: deriving exactly the traits a type's own logic needs —
and no more — keeps flash usage proportional to actual use, rather than
paying for Debug's formatting code on a type instantiated and copied
constantly; see #[derive(...)]'s embedded
explanation for the full code-size
reasoning behind treating Debug as opt-in on hot types.
Scenario
Testing
A small, infrequently-constructed configuration type is a good candidate
for deriving Debug and PartialEq together, since host-run unit tests
need both to use assert_eq! — and this type's derive cost is paid once,
not on every poll loop iteration.
#![no_std]
#[derive(Debug, PartialEq)] // <- both needed for assert_eq! in host-run tests; cheap here: rarely constructed
pub struct UartConfig {
pub baud_rate: u32,
pub parity_enabled: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_expected_config() {
let cfg = UartConfig { baud_rate: 115_200, parity_enabled: false };
assert_eq!(cfg, UartConfig { baud_rate: 115_200, parity_enabled: false });
}
}
Why this way:assert_eq! still requires Debug + PartialEq on a
no_std target exactly as it does on a hosted one, and this type is
constructed rarely enough (at startup, from configuration) that its
Debug derive's flash cost is negligible — the code-size argument for
withholding Debug applies to hot, repeatedly-instantiated types, not to
every type in a crate uniformly.