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

Debug

{:?}: type name and fields; {:#?} pretty-prints

assert_eq!, dbg!, unwrap messages

Clone

field-by-field clone()

.clone(), Vec::to_vec, Arc::make_mut

Copy

marker: assignment copies bits; requires Clone

implicit copies

PartialEq / Eq

field-by-field ==; Eq is a marker for "equality is reflexive"

==, contains, HashMap keys (Eq)

PartialOrd / Ord

lexicographic by field declaration order (enums: by variant order)

<, sort, BTreeMap keys (Ord)

Hash

hashes every field

HashMap/HashSet keys

Default

every field’s default; for enums, the variant marked #[default]

unwrap_or_default, ..Default::default()

#[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<_>>());
}

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

  • Debug is for programmers: complete, unambiguous, usually derived. Use it in logs and assertions.

  • Display is for users: a deliberate, human-readable representation. It is never derived; implementing it also gives to_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]));
}

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 Copy for small, plain-data types (coordinates, IDs, enums without data, ratings) — it makes them pleasant to use;

  • do not derive Copy for types that might later gain a String, Vec or other owning field, because removing Copy is a breaking change;

  • a manual Clone for a Copy type must equal the bit copy (*self).

See Ownership & Moves for the semantics.

Equality, ordering and hashing — and their laws

flowchart TB PE["PartialEq: == and !=
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)"]
  • PartialEq without Eq exists for types where some value is not equal to itself — floats and NaN.

  • PartialOrd returns Option<Ordering>: None means "incomparable" (again, NaN).

  • Ord requires a total order consistent with Eq and PartialOrd. sort, BTreeMap and binary_search rely on it; since 1.81 the sort functions may panic if the order is inconsistent.

  • Hash must agree with Eq. If you implement PartialEq by hand (say, case-insensitively), implement Hash the same way, or HashMap lookups 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

a + b, -, *, /, %

Add, Sub, Mul, Div, Rem

add(self, rhs) -> Output

-a, !a

Neg, Not

neg(self), not(self)

&, |, ^, <<, >>

BitAnd, BitOr, BitXor, Shl, Shr

a += b, -=, …​

AddAssign, SubAssign, …​

add_assign(&mut self, rhs)

a[i], a[i] = x

Index, IndexMut

index(&self, i) -> &Output

*a

Deref, DerefMut

smart pointers only

a(b)

Fn*

not implementable on stable

==, <

PartialEq, PartialOrd

std::cmp

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).