Operator Overloading

Learn how to customize Rust operators like +, ==, and [] for your own types by implementing standard library traits

Rust lets you define what operators like +, ==, !, and [] do when used with your own types. Instead of a special syntax for operator overloading, Rust provides a set of traits in the std::ops module. When you implement the right trait for your type, the corresponding operator becomes available. This approach keeps operator behavior explicit, discoverable, and consistent across the ecosystem.

For a beginner, the simplest way to think about it is this: an operator is just a method call with a shorter syntax. Writing a + b calls the add method from the Add trait, if your type implements it. The Rust standard library already does this for numbers, but you get to extend those same operators to your own structs and enums.

Where to Find the Traits:

Every overloadable operator lives in std::ops. The module documentation lists all the traits and their signatures. You can also use std::ops::Add, std::ops::Neg, and so on without importing each one separately if you prefer—just bring in std::ops::* for learning.


The Operator Trait Pattern

Almost every operator trait follows the same structure. Let’s look at Add as a representative example:

pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

Three things are happening here:

  • Rhs (right‑hand side): the type of the value on the right of the +. It defaults to Self, so a + b expects b to be the same type as a unless you specify otherwise.
  • Output: the type that a + b produces. It does not have to be the same as Self. You can add two Point values and get a Point, or add a Point and a Vector and get a Point, for example.
  • add: the method that implements the actual operation. It takes ownership of self (the left operand) and rhs (the right operand), so you can move values if needed.

When you write let c = a + b;, Rust desugars it to let c = Add::add(a, b); under the hood. The compiler then uses the associated Output type to know the type of c.

This pattern repeats across subtraction, multiplication, bitwise operations, and even unary operators like - and ! (where the Rhs parameter is absent or unused). Once you understand Add, the others follow naturally.

You Already Use This:

Every time you add two i32 values, Rust calls the Add implementation for i32. Overloading just lets you give your own types the same familiar syntax.


Arithmetic and Bitwise Operators

Rust overloads the standard arithmetic operators (+, -, *, /, %) and bitwise operators (&, |, ^, <<, >>) through traits that mirror the Add pattern. The table below shows each operator, its trait, and the required method.

OperatorTraitMethod
+Addadd
-Subsub
*Mulmul
/Divdiv
%Remrem
&BitAndbitand
|BitOrbitor
^BitXorbitxor
<<Shlshl
>>Shrshr

All of these are binary operators: they take a left operand (self) and a right operand (rhs). Each trait has an Output associated type for the result.

Implementing Addition for a Custom Type

Imagine a simple 2D point that you want to add together.

use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}
impl Add for Point {
    type Output = Point;
    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}
fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = Point { x: 3.0, y: 4.0 };
    let p3 = p1 + p2;
    println!("{:?}", p3); // Point { x: 4.0, y: 6.0 }
}

Because Point derives Copy, the add method can take ownership and still leave the original values usable. If Point held heap-allocated data without Copy, you would have to decide whether to consume the operands or take them by reference (which would require a different approach—see the compound assignment section).

Taking Ownership Can Surprise You:

The add method signature consumes self and rhs. If your type is not Copy and you write let c = a + b;, you cannot use a or b afterwards. For non‑Copy types that should remain usable, implement Add for references instead, like impl<'a> Add for &'a MyType with type Output = MyType;, and then use &a + &b. We’ll revisit this pattern later.

Overloading with Different Types on Each Side

Sometimes the right‑hand side should be a different type. The Rhs generic parameter defaults to Self, but you can specify another type. The next example adds a Millimeters value and a Meters value, converting the meters to millimeters for the result.

use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
    type Output = Millimeters;
    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

Now let total = Millimeters(1500) + Meters(2); gives Millimeters(3500). The right‑hand side is a Meters, not Millimeters, because we set Rhs to Meters in the impl block.

Orphan Rule Restriction:

You cannot implement a foreign trait (like Add) for a foreign type if both the trait and the type are defined outside your crate. For example, impl Add<MyType> for f64 is forbidden because f64 and Add are both standard library items, and MyType is local. Rust enforces this to prevent conflicting implementations across crates. When operator overloading feels impossible, check whether you can wrap the external type in a newtype struct.


Unary Operators

Unary operators work on a single operand. Rust overloads negation (-) with the Neg trait and logical not (!) with the Not trait.

OperatorTraitMethod
-Negneg
!Notnot

These traits are simpler because there is no right‑hand side; they only take self and produce an Output.

use std::ops::Neg;
#[derive(Debug, Clone, Copy)]
struct Temperature(f64);
impl Neg for Temperature {
    type Output = Temperature;
    fn neg(self) -> Temperature {
        Temperature(-self.0)
    }
}
fn main() {
    let cold = Temperature(5.0);
    let very_cold = -cold;
    println!("{:?}", very_cold); // Temperature(-5.0)
}

The Not trait works identically but is typically used for boolean‑like types or bit‑flipping.


Binary Operators

The term binary operator refers to any operator that takes two operands. The arithmetic and bitwise operators we already covered are binary operators. This section zooms out to discuss two details that apply to all binary operator traits: the Rhs parameter pattern and working with generics.

The Rhs Default

Every binary operator trait defines Rhs with a default of Self. That is why impl Add for Point lets you write p1 + p2 when both are Point. The full syntax is Add<Point>, but Add alone uses the default. You only write the explicit Rhs when the right‑hand type differs.

Operator Overloading with Generics

When your type is generic, you often want + to work only when the inner type supports addition. You express this with trait bounds.

use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Pair<T> {
    first: T,
    second: T,
}
impl<T: Add<Output = T>> Add for Pair<T> {
    type Output = Pair<T>;
    fn add(self, other: Pair<T>) -> Pair<T> {
        Pair {
            first: self.first + other.first,
            second: self.second + other.second,
        }
    }
}

The clause T: Add<Output = T> ensures that T itself implements Add and that adding two T values yields another T. Then self.first + other.first is legal inside the method.

The associated type bound Output = T often confuses newcomers. It answers the question: "When you add two T values, does the result come out as the same type T?" Many numeric types satisfy this, but not all possible Add implementations do. The bound is necessary because the compiler cannot assume the output type matches T without it.

The Output Bound in Practice:

If you write a generic function that adds two values, you almost always need T: Add<Output = T> (or T: Add<Output = U> with a different target). Forgetting the Output bound leads to a compiler error that asks you to add it. This error is one of the best teachers of Rust’s trait system—when you see it, you’re learning how the type checker proves correctness.


Compound Assignment Operators

The +=, -=, *=, and similar operators correspond to the AddAssign, SubAssign, MulAssign, and other *Assign traits. Unlike the plain arithmetic traits, these modify the left operand in place.

pub trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, rhs: Rhs);
}

Notice there is no Output—the left‑hand value is mutated directly. You implement it when you want a += b to work.

use std::ops::AddAssign;
#[derive(Debug, Clone)]
struct Counter {
    value: u32,
}
impl AddAssign for Counter {
    fn add_assign(&mut self, other: Counter) {
        self.value += other.value;
    }
}
fn main() {
    let mut c1 = Counter { value: 10 };
    let c2 = Counter { value: 5 };
    c1 += c2;
    println!("{:?}", c1); // Counter { value: 15 }
}

A common pattern is to implement AddAssign for a type and then implement Add in terms of it—or vice versa. The standard library provides a blanket implementation: if a type implements Add<T, Output = T>, it automatically gets AddAssign<T> for mutable references, though the details are nuanced. For custom types, you often implement both explicitly to avoid surprises.

Consuming vs. Borrowing:

The + operator typically consumes operands. If your type is expensive to clone, you might overload + for references and use AddAssign for owned values that you intend to mutate. The choice depends on whether you want the syntax a = a + b (consuming) or a += b (mutating). Plan for the idiomatic usage of your type.


Equality Tests: PartialEq and Eq

The == and != operators come from the PartialEq trait.

pub trait PartialEq<Rhs = Self> {
    fn eq(&self, other: &Rhs) -> bool;
    fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}

Because eq takes references, you can compare values without consuming them. The ne method has a default implementation that simply negates eq, so you only need to provide eq.

Most types derive PartialEq automatically:

#[derive(PartialEq, Debug)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

Now you can write if color1 == color2 { ... }. For types that contain floating‑point numbers, derived PartialEq compares bit‑patterns directly, which may not be what you want because NaN is never equal to anything, including itself. In such cases, write a manual implementation.

The Eq trait marks a type where equality is an equivalence relation—specifically, every value is equal to itself. This is always true for integers, strings, and most data structures. Floating‑point types implement PartialEq but not Eq because NaN != NaN. When you derive PartialEq, you can also derive Eq if the underlying fields all satisfy it:

#[derive(PartialEq, Eq, Debug)]
struct Point {
    x: i32,
    y: i32,
}

Forgotten PartialEq Implementation:

If you write == for a type that does not implement PartialEq, the compiler will stop with a clear error. More subtly, if you implement PartialEq manually and forget to handle NaN-like cases (if you have floats), code that relies on reflexivity can break. Use #[derive(PartialEq)] when possible and only hand‑write it for special numeric semantics.


Ordered Comparisons: PartialOrd and Ord

The <, >, <=, and >= operators are driven by the PartialOrd and Ord traits.

pub trait PartialOrd<Rhs = Self>: PartialEq<Rhs> {
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
    fn lt(&self, other: &Rhs) -> bool { ... }
    fn le(&self, other: &Rhs) -> bool { ... }
    fn gt(&self, other: &Rhs) -> bool { ... }
    fn ge(&self, other: &Rhs) -> bool { ... }
}

partial_cmp returns Option<Ordering> because comparison might be impossible (think NaN in floats again). The convenience methods lt, le, gt, ge have default implementations that call partial_cmp. PartialOrd requires PartialEq as a supertrait because you can’t have ordering without equality.

Ord extends PartialOrd and requires a total ordering: for any two values a and b, exactly one of a < b, a == b, or a > b is true. This is what lets you sort a collection deterministically.

pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
}

Most structs can derive PartialOrd and Ord together:

#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
struct Score {
    points: u32,
}

Now if score1 > score2 { ... } works and you can sort a Vec<Score>.

Floats Break Total Ordering:

Floating‑point types implement PartialOrd but not Ord. If your struct contains an f32 or f64, you cannot derive Ord. You have to decide on a policy for NaN—either avoid it, treat it as larger than all other values, or use a wrapper that panics on NaN.


Index and IndexMut

The indexing operators container[index] and container[index] = value rely on the Index and IndexMut traits.

pub trait Index<Idx> {
    type Output;
    fn index(&self, index: Idx) -> &Self::Output;
}
pub trait IndexMut<Idx>: Index<Idx> {
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}

Idx is the type of the index—often usize or a range type, but you can use any type you like. The method returns a reference to the element, so no data is moved.

use std::ops::Index;
struct Matrix {
    data: Vec<Vec<f64>>,
}
impl Index<(usize, usize)> for Matrix {
    type Output = f64;
    fn index(&self, (row, col): (usize, usize)) -> &f64 {
        &self.data[row][col]
    }
}

Now let value = matrix[(0, 1)]; works, which is cleaner than matrix.data[0][1]. To allow assignment, implement IndexMut with the same Idx type.

Indexing Does Not Panic by Convention:

The standard library’s Index implementations for slices and vectors panic on out‑of‑bounds access. Your own Index implementation can choose a different strategy—returning an Option is impossible because index must return a reference, not an owned value. If a missing key is possible, consider a method like get that returns Option<&V> instead of overloading [].


Other Operators: Drop, Deref, and More

Some operators do not correspond to arithmetic or comparison but still use traits from std::ops. These are covered in depth elsewhere in the documentation, but here is a quick map to help you recognize them as part of the operator overloading family:

  • Drop — runs cleanup code when a value goes out of scope. You implement Drop to give your type custom destructor logic. This is not an operator you call explicitly, but it is a trait that changes how your type behaves at the end of its life.
  • Deref and DerefMut — enable the * dereference operator and automatic deref coercion. Implementing Deref lets you treat a smart pointer like a regular reference.
  • Fn, FnMut, FnOnce — overload the call operator () so you can call a struct like a function.
  • RangeBounds and related traits — not operators in the syntax sense but are used by the range syntax .. when combined with indexing.

These traits follow the same ethos: implement the trait, and Rust provides the syntax or behavior you want.

Where to Go from Here:

Operator overloading is a small surface area, but it connects deeply with the rest of the trait system. Once you’re comfortable writing impl Add for MyType, you’re ready to explore advanced trait features like associated types and supertraits, which appear throughout the standard library. Check out the chapters on Deref and Drop in the Smart Pointers section to see these operator traits in action.


Summary

Rust ties operator overloading directly to the trait system. This choice makes overloading predictable: you never guess which trait enables +; you look at std::ops::Add. The uniformity of the trait signatures—add(self, rhs) -> Self::Output—means that learning one operator teaches you the pattern for nearly all of them.

The most important takeaways:

  • Use the Output associated type to control the result type; it can differ from both operands.
  • Respect the orphan rule: you must own either the trait or the type to write an implementation.
  • Derive PartialEq, Eq, PartialOrd, and Ord whenever possible to get safe, idiomatic comparison operators.
  • Implement AddAssign (and friends) when in‑place mutation is natural for your type.