Operator overloading
Intermediate · Abstractions
What & why
1 + 2 and p1 + p2 look like the same kind of thing, but for your own struct, + doesn’t mean anything until you tell the compiler what it means. In Rust, every operator is secretly a trait method — a + b is shorthand the compiler expands to Add::add(a, b). Implement the matching trait from std::ops (Add, Sub, Mul, Index, …) for your own type, and it gets to use +, -, *, [], and friends, exactly like a built-in number or collection would.
The idea, slowly
Operators desugar to trait methods
There’s no special compiler magic for + — it’s a lookup. When the compiler sees a + b, it looks for an Add implementation for a’s type and rewrites the expression as a call to it:
use std::ops::Add;
fn main() {
let sum = 3.add(4); // exactly what 3 + 4 desugars to for integers
println!("{}", sum);
println!("{}", 3 + 4); // identical result, normal syntax
}
Integers implement Add in the standard library, which is why 3 + 4 works at all. Every arithmetic and indexing operator in Rust follows this same rule: a symbol is really a method call on a trait, dressed up in familiar syntax.
Implementing Add for your own type
To make + work on a struct, implement std::ops::Add for it. The trait has one associated type (Output, what the + expression evaluates to) and one method (add, what actually runs):
use std::ops::Add;
#[derive(Clone, Copy, Debug)]
struct Point { x: i32, y: i32 }
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2; // calls Add::add(p1, p2)
println!("{:?}", p3); // Point { x: 4, y: 6 }
}
What the compiler is thinking: p1 + p2 is rewritten to Add::add(p1, p2) before type checking even happens. It then checks that Point implements Add, that add accepts a Point on the right-hand side, and that the result type matches how p3 is used. Sub and Mul work identically — impl Sub for Point { type Output = Point; fn sub(self, rhs: Point) -> Point { ... } } is all p1 - p2 needs.
Notice Point derives Copy. Most std::ops traits (including Add) take self by value, which moves (or copies, for Copy types) the left-hand operand. For a small struct like Point, deriving Copy means p1 + p2 doesn’t consume p1 — you can keep using it afterward. For a non-Copy type, self + rhs would move self, so you’d only be able to use the sum, not the original operands, unless you implement Add for references instead (impl Add for &Point).
Index and IndexMut: making container[key] work
The same idea extends to []. Implement Index (read) and IndexMut (read-write) to make your own type support subscript syntax:
use std::ops::Index;
struct Grid {
cells: Vec<i32>,
width: usize,
}
impl Index<(usize, usize)> for Grid {
type Output = i32;
fn index(&self, (row, col): (usize, usize)) -> &i32 {
&self.cells[row * self.width + col]
}
}
fn main() {
let grid = Grid { cells: vec![1, 2, 3, 4, 5, 6], width: 3 };
println!("{}", grid[(1, 2)]); // row 1, col 2 -> cells[1*3 + 2] = cells[5] = 6
}
grid[(1, 2)] desugars to *Index::index(&grid, (1, 2)) — index returns a reference, and the [] syntax automatically dereferences it for you. IndexMut follows the same shape but returns &mut Output, which is what lets grid[(1, 2)] = 9; work as an assignment.
When overloading helps — and when a named method is clearer
Operator overloading is a judgment call, not a default. It earns its place when the operator’s meaning is exactly what the reader would expect from ordinary math or collection syntax: Point + Point reads naturally as vector addition; Matrix * Matrix reads naturally as matrix multiplication; grid[(row, col)] reads naturally as indexing.
It goes wrong when + does something a reader wouldn’t guess from the symbol — merging two Config structs with “last one wins” semantics, or a + that has side effects like writing to a file. In those cases a named method (config.merged_with(other), log.append(entry)) is far clearer than a surprising operator, because the name tells the reader what actually happens instead of leaning on a symbol to imply it.
Common mistakes
- Overloading an operator with surprising semantics.
+that mutates one of its operands, has side effects, or doesn’t correspond to what “addition” would mean for your type is worse than a named method — readers bring assumptions to+that your code should honor, not violate. - Assuming
Addgives you+=for free.AddAssign(which powers+=) is a separate trait fromAdd. Implementing one does not implement the other — if you want bothp1 + p2andp += p2to work, you implement bothAddandAddAssign. - Forgetting most
std::opstraits takeselfby value. If your type isn’tCopy,p1 + p2movesp1(andp2), so you can’t use them again afterward. Either deriveCopyfor small value-like types, or implement the operator for references (impl Add for &Point) so operands are borrowed instead of consumed. - Mismatched
Outputtype.type Output = Pointmust match whatadd’s body actually returns and what call sites expect. A mismatch shows up as a type error at the+expression itself, which can look confusing if you don’t already know operators are trait calls.
More examples
Totaling a receipt in cents
A Money type that implements Add lets a checkout add up line items with plain +, instead of a .total() method that has to be remembered and called separately.
use std::ops::Add;
#[derive(Clone, Copy, Debug)]
struct Money { cents: u32 }
impl Add for Money {
type Output = Money;
fn add(self, rhs: Money) -> Money {
Money { cents: self.cents + rhs.cents }
}
}
fn main() {
let coffee = Money { cents: 350 };
let muffin = Money { cents: 275 };
let total = coffee + muffin;
println!("total: ${}.{:02}", total.cents / 100, total.cents % 100);
}
Applying a speed boost power-up
Mul<f64> lets a game scale a velocity by a plain number, so a power-up reads as speed * 2.5 instead of a helper function that rebuilds the struct by hand.
use std::ops::Mul;
#[derive(Clone, Copy, Debug)]
struct Velocity { dx: f64, dy: f64 }
impl Mul<f64> for Velocity {
type Output = Velocity;
fn mul(self, factor: f64) -> Velocity {
Velocity { dx: self.dx * factor, dy: self.dy * factor }
}
}
fn main() {
let base_speed = Velocity { dx: 2.0, dy: 1.0 };
let boosted = base_speed * 2.5; // speed boost power-up
println!("{:?}", boosted);
}
A playlist that loops when it runs out of tracks
Index doesn’t have to mean “array position” — wrapping the index with % inside index() makes playlist[i] loop back to the start for any i, which is exactly how repeat playback behaves.
use std::ops::Index;
struct Playlist {
tracks: Vec<String>,
}
impl Index<usize> for Playlist {
type Output = String;
fn index(&self, i: usize) -> &String {
&self.tracks[i % self.tracks.len()] // wraps around for looping playback
}
}
fn main() {
let playlist = Playlist {
tracks: vec!["Intro".to_string(), "Solo".to_string(), "Outro".to_string()],
};
for i in 0..5 {
println!("track {}: {}", i, playlist[i]);
}
}
Deducting a shipped order from warehouse stock
Sub makes “what’s left after this order ships” read the same way subtraction reads for ordinary numbers, instead of a .deduct(order) method.
use std::ops::Sub;
#[derive(Clone, Copy, Debug)]
struct Stock { units: u32 }
impl Sub for Stock {
type Output = Stock;
fn sub(self, rhs: Stock) -> Stock {
Stock { units: self.units - rhs.units }
}
}
fn main() {
let warehouse = Stock { units: 120 };
let shipped_order = Stock { units: 45 };
let remaining = warehouse - shipped_order;
println!("{:?}", remaining);
}
Your turn
Point implements Add, but this code also tries to use +=. It doesn’t compile:
use std::ops::Add;
#[derive(Clone, Copy, Debug)]
struct Point { x: i32, y: i32 }
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
fn main() {
let mut p = Point { x: 1, y: 2 };
p += Point { x: 3, y: 4 };
println!("{:?}", p);
}
Show solution
The error is binary assignment operation += cannot be applied to type Point. Implementing Add only teaches the compiler what p1 + p2 means — += is a different operator backed by a different trait, AddAssign, which Point doesn’t implement yet. Add it:
use std::ops::{Add, AddAssign};
#[derive(Clone, Copy, Debug)]
struct Point { x: i32, y: i32 }
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
impl AddAssign for Point {
fn add_assign(&mut self, rhs: Point) {
self.x += rhs.x;
self.y += rhs.y;
}
}
fn main() {
let mut p = Point { x: 1, y: 2 };
p += Point { x: 3, y: 4 };
println!("{:?}", p); // Point { x: 4, y: 6 }
}
Each operator symbol maps to its own trait — + to Add, += to AddAssign, - to Sub, -= to SubAssign, and so on. Implementing one never implies the other; you implement each operator you actually want to support.
Quick check
Remember this
- Operators are trait methods in disguise:
a + bdesugars toAdd::add(a, b). impl Add for Point { type Output = Point; fn add(self, rhs: Point) -> Point { ... } }enablesp1 + p2.Index/IndexMutenablecontainer[key]syntax for your own collection-like types.+and+=are separate traits (AddandAddAssign) — implementing one doesn’t give you the other.- Only implement an operator when its meaning is unambiguous — don’t overload
+for something that isn’t really addition; use a named method instead.
Go deeper
- std::ops module docs — Every overloadable operator trait.
Next: