Operator Overloading
Reviewed & published by Brayan K
Operator overloading is implementing a trait from the std::ops module — such as Add for + or Mul for * — so that an operator works on your own custom type instead of only on built-in numbers.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll implement Add, Sub, and Mul for a 2-D vector, understand the associated Output type, add += with AddAssign and indexing with Index, and learn why operators are traits in the first place.
What You'll Learn in This Lesson
- That every operator maps to a std::ops trait
- Implementing Add/Sub/Mul on a custom type
- The associated Output type and what it's for
- Scaling by a scalar with Mul<f64>
- In-place += with AddAssign
- Indexing with Index<usize>
1️⃣ The Add Trait and the Output Type
When you write a + b, Rust rewrites it to a.add(b) using the Add trait. Add declares an associated type Output — the type the operation produces — and one method, add. Implement them for your struct and + simply works.
use std::ops::Add;
// Operators in Rust are traits. Implementing std::ops::Add for a type makes
// the + operator work on it. Add has an associated type Output: the result type.
#[derive(Debug, Clone, Copy)]
struct Vector2 {
x: f64,
y: f64,
}
impl Add for Vector2 {
type Output = Vector2; // what `a + b` produces
fn add(self, other: Vector2) -> Vector2 {
Vector2 {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let a = Vector2 { x: 1.0, y: 2.0 };
let b = Vector2 { x: 3.0, y: 4.0 };
// This calls a.add(b) under the hood.
let sum = a + b;
println!("{:?}", sum);
}Here type Output = Vector2 because adding two vectors gives a vector. For symmetric same-type math you'll often just write type Output = Self.
2️⃣ Many Operators, Different Right-Hand Types
A single type can implement several operator traits. And the right-hand side doesn't have to be the same type as Self: Mul<f64> lets you multiply a Vector2 by a plain f64 scalar. The generic parameter on the trait is the type of the operand on the right.
use std::ops::{Add, Sub, Mul};
// One type can implement many operator traits. Mul here scales by a scalar f64,
// so the right-hand side type differs from Self — that's Mul<f64>.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vector2 {
x: f64,
y: f64,
}
impl Add for Vector2 {
type Output = Vector2;
fn add(self, o: Vector2) -> Vector2 {
Vector2 { x: self.x + o.x, y: self.y + o.y }
}
}
impl Sub for Vector2 {
type Output = Vector2;
fn sub(self, o: Vector2) -> Vector2 {
Vector2 { x: self.x - o.x, y: self.y - o.y }
}
}
// Multiply a vector by a scalar: Vector2 * f64
impl Mul<f64> for Vector2 {
type Output = Vector2;
fn mul(self, scalar: f64) -> Vector2 {
Vector2 { x: self.x * scalar, y: self.y * scalar }
}
}
fn main() {
let a = Vector2 { x: 2.0, y: 3.0 };
let b = Vector2 { x: 1.0, y: 1.0 };
println!("a + b = {:?}", a + b);
println!("a - b = {:?}", a - b);
println!("a * 2.0 = {:?}", a * 2.0);
}3️⃣ AddAssign (+=) and Index ([])
Compound assignment operators have their own traits: += is AddAssign, which mutates &mut self in place and has no Output type. The indexing syntax container[i] for reading comes from the Index trait, which returns a reference to the element.
use std::ops::{AddAssign, Index};
// AddAssign powers the += operator (it mutates self, no Output type).
// Index powers the container[i] read syntax; it returns a reference.
#[derive(Debug)]
struct Vector3 {
data: [f64; 3],
}
impl AddAssign for Vector3 {
fn add_assign(&mut self, other: Vector3) {
for i in 0..3 {
self.data[i] += other.data[i];
}
}
}
impl Index<usize> for Vector3 {
type Output = f64; // what indexing yields (by reference)
fn index(&self, i: usize) -> &f64 {
&self.data[i]
}
}
fn main() {
let mut v = Vector3 { data: [1.0, 2.0, 3.0] };
// += calls add_assign and mutates v in place.
v += Vector3 { data: [10.0, 20.0, 30.0] };
println!("after += : {:?}", v);
// Indexing calls index() and returns &v.data[1].
println!("v[0] = {}", v[0]);
println!("v[2] = {}", v[2]);
}Notice add_assign takes &mut self and returns nothing — it edits the existing value. Index still has an Output type (the element type) and returns it by reference.
Your turn. Fill in the blank marked ___, then run it.
// 🎯 YOUR TURN — implement Sub so wallet - price gives change.
use std::ops::Sub;
#[derive(Debug, Clone, Copy)]
struct Money {
cents: i64,
}
impl Sub for Money {
type Output = ___; // 👉 the result type of subtraction
fn sub(self, other: Money) -> Money {
Money { cents: self.cents - other.cents }
}
}
fn main() {
let wallet = Money { cents: 1000 };
let price = Money { cents: 349 };
let change = wallet - price; // calls wallet.sub(price)
println!("{:?}", change);
}
// ✅ Expected output:
// Money { cents: 651 }Mini-Challenge: Complex Number Math
Give a Complex type real + and * operators. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: complex numbers with + and *.
//
// 1. Define struct Complex { re: f64, im: f64 } and derive Debug, Clone, Copy.
// 2. impl Add for Complex: add the real parts and the imaginary parts.
// 3. impl Mul for Complex using the rule:
// re = a.re*b.re - a.im*b.im
// im = a.re*b.im + a.im*b.re
// 4. In main, with a = 1 + 2i and b = 3 + 4i, print a + b and a * b.
//
// ✅ Example output:
// a + b = Complex { re: 4.0, im: 6.0 }
// a * b = Complex { re: -5.0, im: 10.0 }
fn main() {
// your code here
}Common Errors (and the fix)
- ❌ cannot add `Vector2` to `Vector2` — you used + without implementing Add. ✅ Fix: add impl Add for Vector2.
- ❌ not all trait items implemented, missing: `Output` — you forgot the associated type. ✅ Fix: add type Output = Self;.
- ❌ 2.0 * v won't compile even though v * 2.0 does — operators aren't commutative. ✅ Fix: add impl Mul<Vector2> for f64.
- ❌ Using self after add consumed it — add takes self by value. ✅ Fix: derive Copy or implement Add for references.
Pro Tips
- 💡 Derive Copy on small math types: operator methods take self by value, so Copy keeps the original usable.
- 💡 Pair Add with AddAssign: users expect += wherever + works.
- 💡 Respect least surprise: overload an operator only when its meaning is obvious; otherwise use a named method.
📋 Quick Reference — std::ops Traits
| Operator | Trait / method |
|---|---|
| + | Add::add |
| - | Sub::sub |
| * | Mul::mul |
| += | AddAssign::add_assign |
| [i] | Index::index |
🎉 Lesson Complete!
- ✅ Operators are traits in std::ops; a + b is a.add(b)
- ✅ Add/Sub/Mul each have an associated Output type
- ✅ The trait's generic parameter sets the right-hand operand type (e.g. Mul<f64>)
- ✅ AddAssign powers += in place; Index powers []
- ✅ Overload only when the meaning is obvious (least surprise)
Practice quiz
In Rust, operators are implemented as:
- Built-in magic only
- Macros
- Traits in the std::ops module
- Keywords
Answer: Traits in the std::ops module. Each operator maps to a trait in std::ops, so overloading means implementing it.
Which trait powers the + operator?
- Add
- Plus
- Sum
- Concat
Answer: Add. The + operator corresponds to the Add trait and its add method.
When you write a + b, what does the compiler call?
- a.plus(b)
- add(a, b)
- b.add(a)
- a.add(b)
Answer: a.add(b). a + b is rewritten as a.add(b) using the Add trait.
What is the Output associated type for?
- The input type
- The type the operation produces
- The error type
- The trait name
Answer: The type the operation produces. Output names the result type of the operation, set with type Output = ...;.
For same-type math, Output is usually:
- type Output = Self;
- type Output = ();
- type Output = bool;
- omitted
Answer: type Output = Self;. Symmetric same-type math commonly uses type Output = Self.
Which trait makes Vector2 * f64 (scalar) work?
- Mul
- Scale
- Mul<f64>
- MulAssign
Answer: Mul<f64>. The generic parameter sets the right-hand type, so Mul<f64> multiplies by a scalar.
Which trait powers the += operator?
- Add
- AddAssign
- PlusEq
- Increment
Answer: AddAssign. AddAssign powers +=, mutating self in place.
Does AddAssign have an Output associated type?
- Yes
- Only for floats
- Same as Add
- No, it mutates in place and returns nothing
Answer: No, it mutates in place and returns nothing. add_assign takes &mut self and returns nothing, so AddAssign has no Output.
What does the Index trait power, and what does it return?
- += returning a value
- container[i] reads, returning a reference
- Multiplication
- Iteration
Answer: container[i] reads, returning a reference. Index powers container[i] reads and returns a reference to the element.
Is v * 2.0 working enough to make 2.0 * v work too?
- Yes, operators are commutative
- Only with derive
- No, you must also impl Mul<Vector2> for f64
- Only for integers
Answer: No, you must also impl Mul<Vector2> for f64. Operators aren't automatically commutative; the reverse needs its own impl.
Continue this course
- Previous: Derive Macros (#[derive])
- Next: The Default & Clone Traits — Default::default(), struct update syntax, and Clone vs Copy
- Quick reference: Rust cheat sheet
Frequently asked questions
How does Rust let me overload operators?
Every operator in Rust corresponds to a trait in the std::ops module. The + operator is the Add trait, - is Sub, * is Mul, / is Div, += is AddAssign, and [] for reading is Index. When you write a + b, the compiler simply rewrites it as a.add(b). So 'overloading' an operator just means implementing the matching trait for your type. This keeps the language consistent: operators are not special magic, they are ordinary trait methods with nicer syntax.
What is the Output associated type on Add and Mul for?
Traits like Add, Sub, and Mul have an associated type called Output that names the type the operation produces. It exists because the result of an operation is not always the same as the inputs — for example, multiplying a Vector2 by an f64 scalar yields a Vector2, and subtracting two timestamps might yield a Duration rather than a timestamp. You set it with type Output = SomeType; inside the impl, and the method signature returns Self::Output. For symmetric same-type math it is usually just type Output = Self.
What is the difference between Add and AddAssign?
Add powers the binary + operator and produces a new value: let c = a + b leaves a and b untouched and returns a fresh result, so Add has an Output type. AddAssign powers the += operator and mutates the left-hand side in place: a += b changes a directly. Because it modifies in place, add_assign takes &mut self and returns nothing, so AddAssign has no Output type. Implement both when it makes sense, since users will expect += to work wherever + does.
Should I overload operators on my own types?
Overload an operator only when it has an obvious, unsurprising meaning for your type — vectors, matrices, money, complex numbers, durations, and similar math-like types are good candidates. The golden rule is least surprise: + should feel like addition, not some unrelated action like appending to a log. If the meaning is not immediately clear to a reader, prefer a named method such as combine or push instead. Misused operators make code harder to read, which defeats the purpose.