Standard Traits & Operator Overloading
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. A handful of standard traits
give types their everyday abilities: printing, copying, comparing, hashing, defaulting and cleaning up. Most can be
derived; some must be written by hand. Operators (+, *, [], ==, <) are themselves trait methods, so
implementing the right trait makes a type work with the operator.
The derivable traits
#[derive(...)] asks the compiler to generate an implementation from the type’s fields. Each derive requires the same
trait on every field (and, for generic types, adds a T: Trait bound on each type parameter).
| Trait | Derived behaviour | Required by |
|---|---|---|
|
|
|
|
field-by-field |
|
|
marker: assignment copies bits; requires |
implicit copies |
|
field-by-field |
|
|
lexicographic by field declaration order (enums: by variant order) |
|
|
hashes every field |
|
|
every field’s default; for enums, the variant marked |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
enum Condition {
Poor,
Fair,
#[default]
Good,
LikeNew,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct ShelfCopy {
shelf: String,
condition: Condition,
}
fn main() {
let a = ShelfCopy { shelf: "A1".into(), condition: Condition::Fair };
let b = ShelfCopy { shelf: "A1".into(), ..Default::default() };
println!("{a:?} < {b:?}: {}", a < b); // same shelf, Fair < Good: true
println!("{:#?}", ShelfCopy::default());
let mut copies = [b.clone(), a.clone()];
copies.sort();
println!("{:?}", copies.iter().map(|c| c.condition).collect::<Vec<_>>());
}
Source: The Rust Reference: Derive
The derived PartialOrd/Ord compare fields in declaration order, so field order is part of the type’s sorting
behaviour. Implement them by hand when the natural order differs (for example "sort books by title, ignoring case").
Debug versus Display
-
Debugis for programmers: complete, unambiguous, usually derived. Use it in logs and assertions. -
Displayis for users: a deliberate, human-readable representation. It is never derived; implementing it also givesto_string().
use std::fmt;
#[derive(Debug)]
struct Book {
title: String,
authors: Vec<String>,
year: u16,
}
impl fmt::Display for Book {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.title, self.year)?;
if f.alternate() { // {:#} for a longer form
write!(f, " by {}", self.authors.join(" and "))?;
}
Ok(())
}
}
struct Isbn([u8; 13]);
impl fmt::Debug for Isbn { // manual Debug: show digits compactly
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let digits: String = self.0.iter().map(|d| char::from(b'0' + d)).collect();
f.debug_tuple("Isbn").field(&digits).finish()
}
}
fn main() {
let b = Book { title: "Good Omens".into(), authors: vec!["Pratchett".into(), "Gaiman".into()], year: 1990 };
println!("{b}"); // Good Omens (1990)
println!("{b:#}"); // Good Omens (1990) by Pratchett and Gaiman
println!("{b:?}");
println!("[{:>30}]", b.to_string()); // padding needs a String, or use f.pad in Display
println!("{:?}", Isbn([9, 7, 8, 0, 4, 4, 1, 0, 1, 3, 5, 9, 3]));
}
Source: std::fmt: Formatting traits
The Formatter helpers debug_struct, debug_tuple, debug_list and debug_map produce output that respects
{:#?}. A Display impl using write! ignores width and alignment flags; call f.pad(&text) when the type should
honour {:>30}. Azam (Advanced Rust, chapter 2) covers derived versus manual Debug and Display as debugging tools;
see also Debugging & Diagnostics.
Clone and Copy
Clone::clone(&self) -> Self makes an explicit duplicate; Copy marks types whose duplicate is a plain bit copy, so
assignment copies instead of moving. Rules of thumb:
-
derive
Copyfor small, plain-data types (coordinates, IDs, enums without data, ratings) — it makes them pleasant to use; -
do not derive
Copyfor types that might later gain aString,Vecor other owning field, because removingCopyis a breaking change; -
a manual
Clonefor aCopytype must equal the bit copy (*self).
See Ownership & Moves for the semantics.
Equality, ordering and hashing — and their laws
symmetric, transitive"] --> EQ["Eq: also reflexive (a == a)
marker, no methods"] PE --> PO["PartialOrd: partial_cmp, < <= > >=
consistent with =="] PO --> ORD["Ord: cmp, max, min, clamp
total order"] EQ --> ORD EQ --> H["Eq + Hash: usable as HashMap / HashSet key
a == b implies hash(a) == hash(b)"]
-
PartialEqwithoutEqexists for types where some value is not equal to itself — floats and NaN. -
PartialOrdreturnsOption<Ordering>:Nonemeans "incomparable" (again, NaN). -
Ordrequires a total order consistent withEqandPartialOrd.sort,BTreeMapandbinary_searchrely on it; since 1.81 the sort functions may panic if the order is inconsistent. -
Hashmust agree withEq. If you implementPartialEqby hand (say, case-insensitively), implementHashthe same way, orHashMaplookups silently fail.
Violating these laws is not undefined behaviour, but it makes collections and algorithms misbehave.
use std::cmp::Ordering;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
/// A title compared case-insensitively.
#[derive(Debug, Clone)]
struct Title(String);
impl PartialEq for Title {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for Title {}
impl Hash for Title {
fn hash<H: Hasher>(&self, state: &mut H) {
for b in self.0.bytes() {
state.write_u8(b.to_ascii_lowercase()); // must agree with eq
}
}
}
impl PartialOrd for Title {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) // delegate to Ord: keeps them consistent
}
}
impl Ord for Title {
fn cmp(&self, other: &Self) -> Ordering {
self.0.to_ascii_lowercase().cmp(&other.0.to_ascii_lowercase())
}
}
fn main() {
let set: HashSet<Title> = ["Dune", "DUNE", "Emma"].map(|t| Title(t.into())).into_iter().collect();
println!("{}", set.len()); // 2
let mut list = vec![Title("emma".into()), Title("Dune".into()), Title("beloved".into())];
list.sort();
println!("{list:?}");
println!("{:?}", Title("a".into()).max(Title("B".into())));
}
Source: std::cmp::Ord
For sorting by a key without a new type, sort_by_key(|b| b.year) and sort_by(|a, b| a.title.cmp(&b.title).then(b.year.cmp(&a.year)))
are usually enough; std::cmp::Reverse flips an order.
Default
Default::default() returns a "zero" or "empty" value. Derive it, or implement it when the sensible default is not the
field-wise one. It enables struct update syntax with ..Default::default(), Option::unwrap_or_default,
HashMap::entry(..).or_default() and mem::take.
Drop
Drop::drop(&mut self) runs when a value goes out of scope. Implement it for types that own resources not already
managed by their fields (a raw handle, a temporary file to delete, a metric to record). A type cannot be both Copy and
Drop, and you cannot call drop directly — use std::mem::drop(value). Details on
Drop, RAII & Drop Check.
Operator overloading with std::ops
Each operator corresponds to a trait. Implementing it makes the operator work for your type:
| Operator | Trait | Method |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
smart pointers only |
|
|
not implementable on stable |
|
|
|
use std::ops::{Add, AddAssign, Index, Mul, Neg};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
struct Money {
cents: i64,
}
impl Add for Money {
type Output = Money;
fn add(self, rhs: Money) -> Money {
Money { cents: self.cents + rhs.cents }
}
}
impl Mul<i64> for Money { // Money * integer, with a different Rhs type
type Output = Money;
fn mul(self, n: i64) -> Money {
Money { cents: self.cents * n }
}
}
impl Neg for Money {
type Output = Money;
fn neg(self) -> Money {
Money { cents: -self.cents }
}
}
impl AddAssign for Money {
fn add_assign(&mut self, rhs: Money) {
self.cents += rhs.cents;
}
}
impl std::iter::Sum for Money { // lets iterators of Money call .sum()
fn sum<I: Iterator<Item = Money>>(iter: I) -> Money {
iter.fold(Money::default(), Add::add)
}
}
struct Ledger(Vec<Money>);
impl Index<usize> for Ledger {
type Output = Money;
fn index(&self, i: usize) -> &Money {
&self.0[i]
}
}
fn main() {
let fee = Money { cents: 50 };
let mut total = fee * 3 + Money { cents: 25 };
total += -fee;
let ledger = Ledger(vec![fee, total]);
let sum: Money = ledger.0.iter().copied().sum();
println!("{total:?} {:?} {sum:?}", ledger[1]); // Money { cents: 125 } ...
}
Guidelines: implement operators only when the meaning is obvious (arithmetic on numeric-like types, Index on
containers); keep Output types unsurprising; implement the *Assign variants and the reference forms
(impl Add<&Money> for &Money) when values are expensive to copy. The orphan rule still applies, but a local type
anywhere in the impl’s type parameters is enough: impl Mul<Money> for i64 is allowed, so 3 * fee can be made to
work as well as fee * 3, while impl Mul<i64> for i64 from your crate is not.
Deref is for smart pointers
Deref lets *x and auto-deref reach a target type. Implement it for types that are pointers to their target
(Box, Rc, String to str, guard types). Do not use it to make a newtype "inherit" its field’s methods: the
community Rust Design Patterns lists that as an anti-pattern ("Deref polymorphism"). See
Type Conversions & Coercions.
Which traits to implement: C-COMMON-TRAITS
The API Guidelines ask public types to eagerly implement the common traits that apply, because downstream crates cannot
add them later (the orphan rule): Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display,
Default, plus Send/Sync where possible, and serde’s Serialize/Deserialize behind an optional serde feature.
At minimum, every public type should implement Debug (the missing_debug_implementations lint checks this).