Rust 71 🦀 Operator Overloading via std::ops Traits
Rust does not allow you to invent new operators or overload arbitrary syntax. What it allows is a carefully bounded form of operator overloading: certain operators are tied to specific traits in std::ops, and implementing those traits for your type defines what the operator means for that type . The + operator is syntactic sugar for the add method on the Add trait. When you write a + b, the compiler translates it to a.add(b). Any type that implements Add can use + . This is the entire mechanism: no magic, no compiler intrinsics, just trait method calls dressed in operator syntax.
The design is deliberately conservative. The assignment operator (=) has no backing trait and cannot be overloaded. The short-circuiting operators && and || are not supported because their behavior depends on not evaluating the right-hand side, which does not fit the trait model . The operators that can be overloaded are the arithmetic operators (+, -, *, /, %), the bitwise operators (&, |, ^, !, <<, >>), the comparison operators (==, !=, <, >, <=, >=), the indexing operators ([]), the dereference operator (*), and the call operator (()). Each has a corresponding trait in std::ops .
Key point: Operator overloading in Rust means implementing a trait from std::ops for your type. The operator syntax is sugar for the trait method call. The Add trait has a default right-hand-side type parameter of Self and an associated Output type, which means impl Add for Point implements Point + Point -> Point, while impl Add<Meters> for Millimeters implements a mixed-type addition with a custom output.
Why operator overloading is trait-based
The orphan rule constraint. You can implement an operator trait for your own type, but you cannot implement it for a foreign type. impl Add for Vec<String> is rejected because both Add and Vec are foreign to your crate . The newtype pattern is the workaround: wrap the foreign type in a local struct and implement the trait for the wrapper. This preserves the orphan rule’s guarantee that trait implementations are coherent and unambiguous.
The Rhs type parameter. The Add trait is defined as Add<Rhs = Self> with an associated type Output . The default Rhs = Self means the common case—adding two values of the same type—requires no type parameter. The explicit Rhs allows mixed-type operations. SystemTime implements Add<Duration>, which permits SystemTime + Duration where the left and right types differ . The Output type is associated rather than a generic parameter because the output is determined by the implementer, not the caller . A generic parameter would let the caller choose the result type, which is almost never what an operator should do.
The ownership problem. The trait methods take self by value. Add::add consumes both operands. For Copy types like i32 or a simple Point with Copy fields, this is trivial. For non-Copy types, consuming the operands means the caller cannot use them again after the operation. The solution is to implement the operator for references as well: impl Add for &Point, impl Add<&Point> for Point, and so on . The standard library documentation explicitly recommends this for types that are likely to be used in generic code, so that the generic code can avoid unnecessary clones .
The surprise problem. The std::ops documentation states that implementations “should be unsurprising in their respective contexts, keeping in mind their usual meanings and operator precedence” . Overloading + to perform I/O, or * to delete a file, is technically possible but violates the contract that operators are shorthand for their conventional meaning. The operator should do what the operator symbol suggests. If the operation is domain-specific and does not map cleanly to the symbol, a named method is clearer .
a. Implementing Add for a custom type
The Add trait is the simplest introduction to the pattern. The implementation declares the Output type and implements the add method.
// Import the Add trait from std::ops
use std::ops::Add;
// A simple Point type with Copy fields
// Copy is derived so the operands can be used after the addition
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
// Implement Add for Point
// The default Rhs = Self means this is Point + Point
impl Add for Point {
// The output type is the same Point
type Output = Point;
// add consumes both operands and returns a new Point
fn add(self, other: Point) -> Point {
Point {
// Add the x components
x: self.x + other.x,
// Add the y components
y: self.y + other.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 0 };
let p2 = Point { x: 2, y: 3 };
// The + operator calls p1.add(p2)
let p3 = p1 + p2;
assert_eq!(p3, Point { x: 3, y: 3 });
}
The #[derive(Copy, Clone)] is not incidental. Without Copy, the p1 + p2 expression would move both p1 and p2 into the add method, and they would be unusable afterward. With Copy, the addition operates on copies, and the originals remain valid. For types that cannot be Copy, the reference implementations are the alternative.
The Output type is Point, which means the result of Point + Point is another Point. This is the conventional meaning of addition for a vector-like type. A different output type is possible—impl Add for Point could return a f64—but the operator’s meaning should be recognizable to a reader who sees + .
b. Mixed-type addition with an explicit Rhs
The Rhs parameter allows the right-hand operand to be a different type from the left-hand operand. This is how unit-aware arithmetic is expressed: adding meters to millimeters, or adding a scalar to a vector.
// Import the Add trait
use std::ops::Add;
// A type representing millimeters
#[derive(Debug, Copy, Clone)]
struct Millimeters(u32);
// A type representing meters
#[derive(Debug, Copy, Clone)]
struct Meters(u32);
// Implement Meters + Millimeters = Millimeters
// The Rhs is Millimeters, not Self
impl Add<Millimeters> for Meters {
// The result is a Millimeters value
type Output = Millimeters;
fn add(self, other: Millimeters) -> Millimeters {
// Convert the meters to millimeters and add
Millimeters(self.0 * 1000 + other.0)
}
}
fn main() {
let distance = Meters(2) + Millimeters(500);
// 2 meters + 500 millimeters = 2500 millimeters
assert_eq!(distance.0, 2500);
}
The Add<Millimeters> implementation means that Meters + Millimeters is valid, but Meters + Meters is not, because the Rhs is fixed to Millimeters. If both operations should be valid, two separate implementations are required: impl Add<Millimeters> for Meters and impl Add for Meters .
The asymmetry is deliberate. The output type is Millimeters, which is a design decision: the result of adding meters to millimeters is expressed in the smaller unit to avoid the precision loss that would come from converting millimeters to meters. The implementer chooses the output, and the caller does not have to specify it .
c. Reference implementations and the Copy trade-off
For types that cannot or should not be Copy, the operator implementation should include reference forms. The standard library documentation recommends that a user-defined type T that supports addition should implement Add<T>, Add<&T>, Add<&T> for &T, and Add<T> for &T, so that generic code can choose the form that matches its ownership constraints .
use std::ops::Add;
// A type that owns a String, so it cannot be Copy
#[derive(Debug, Clone)]
struct Label(String);
// Implement Label + Label, consuming both operands
impl Add for Label {
type Output = Label;
fn add(self, other: Label) -> Label {
// Concatenate the two strings
Label(format!("{}{}", self.0, other.0))
}
}
// Implement &Label + &Label, borrowing both operands
impl Add<&Label> for &Label {
type Output = Label;
fn add(self, other: &Label) -> Label {
// Concatenate without consuming the originals
Label(format!("{}{}", self.0, other.0))
}
}
fn main() {
let a = Label("Hello ".to_string());
let b = Label("World".to_string());
// &a + &b borrows both, so a and b remain usable
let c = &a + &b;
assert_eq!(c.0, "Hello World");
// a and b are still valid because the borrow implementation was used
println!("{} {}", a.0, b.0);
}
The distinction between impl Add for Label and impl Add<&Label> for &Label is the ownership model. The first consumes both operands and is efficient when the caller does not need them anymore. The second borrows both operands and is necessary when the caller needs to reuse them. The standard library provides both for its own types where appropriate, and the documentation recommends the same for user-defined types .
The reference implementation does not automatically apply to the owned values. a + b uses the owned implementation and consumes a and b. &a + &b uses the reference implementation and preserves them. The caller chooses based on what it needs, and the compiler resolves the correct implementation from the types of the operands.
Complete Example Session
The following session builds a Money type that implements Add, Sub, and Mul, with both owned and reference implementations, and demonstrates the operator overloading in a small accounting calculation.
// ============================================
// PART 1: THE TYPE AND ITS CONSTRUCTOR
// ============================================
use std::ops::{Add, Sub, Mul};
// A Money type that stores cents as an i64
// This avoids floating-point rounding errors
#[derive(Debug, Copy, Clone, PartialEq)]
struct Money(i64);
impl Money {
// Create a Money value from dollars and cents
fn new(dollars: i64, cents: i64) -> Self {
// Total cents = dollars * 100 + cents
Money(dollars * 100 + cents)
}
}
// ============================================
// PART 2: ADDITION
// ============================================
// Money + Money consumes both operands
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
// Add the cent values directly
Money(self.0 + other.0)
}
}
// &Money + &Money borrows both operands
impl Add<&Money> for &Money {
type Output = Money;
fn add(self, other: &Money) -> Money {
// Add the cent values without consuming the originals
Money(self.0 + other.0)
}
}
// ============================================
// PART 3: SUBTRACTION
// ============================================
// Money - Money consumes both operands
impl Sub for Money {
type Output = Money;
fn sub(self, other: Money) -> Money {
// Subtract the cent values
Money(self.0 - other.0)
}
}
// ============================================
// PART 4: MULTIPLICATION BY A SCALAR
// ============================================
// Money * i64 multiplies by a scalar quantity
// The Rhs is i64, not Self
impl Mul<i64> for Money {
type Output = Money;
fn mul(self, quantity: i64) -> Money {
// Multiply the cent value by the quantity
Money(self.0 * quantity)
}
}
// ============================================
// PART 5: USING THE OVERLOADED OPERATORS
// ============================================
fn main() {
let price = Money::new(10, 50); // $10.50
let tax = Money::new(0, 84); // $0.84
let quantity = 3;
// Money + Money uses the owned implementation
let subtotal = price + tax;
// subtotal is $11.34
// &Money + &Money borrows, so price and tax remain usable
let borrowed_sum = &price + &tax;
assert_eq!(borrowed_sum, subtotal);
// Money * i64 multiplies by the quantity
let total = price * quantity;
// total is $31.50
// Money - Money subtracts
let discount = total - Money::new(1, 50);
// discount is $30.00
println!("Subtotal: ${}.{}", subtotal.0 / 100, subtotal.0 % 100);
println!("Total: ${}.{}", total.0 / 100, total.0 % 100);
println!("Discount: ${}.{}", discount.0 / 100, discount.0 % 100);
}
The five parts cover the type definition, the Add implementations for owned and borrowed operands, the Sub implementation, the Mul implementation with an explicit Rhs of i64, and the usage that demonstrates all three operators. What the session demonstrates is that operator overloading is trait implementation: the + in price + tax is price.add(tax), the * in price * quantity is price.mul(quantity), and the reference implementation is selected automatically when the operands are references.
Quick Reference
Overloadable Operators and Traits
| Operator | Trait | Method |
|---|---|---|
+ | Add | add |
- | Sub | sub |
* | Mul | mul |
/ | Div | div |
% | Rem | rem |
- (unary) | Neg | neg |
! | Not | not |
& | BitAnd | bitand |
| | BitOr | bitor |
^ | BitXor | bitxor |
<< | Shl | shl |
>> | Shr | shr |
== | PartialEq | eq |
< | PartialOrd | partial_cmp |
[] | Index / IndexMut | index / index_mut |
* (deref) | Deref / DerefMut | deref / deref_mut |
() | Fn / FnMut / FnOnce | call |
Add Trait Signature
| Component | Meaning |
|---|---|
Add<Rhs = Self> | Right-hand side defaults to Self |
type Output | The result type, chosen by the implementer |
fn add(self, rhs: Rhs) -> Output | Consumes both operands |
Ownership Forms
| Form | Consumes Operands? | Use Case |
|---|---|---|
impl Add for T | Yes | Caller does not need operands |
impl Add<&T> for T | Right only | Caller keeps right operand |
impl Add<T> for &T | Left only | Caller keeps left operand |
impl Add<&T> for &T | No | Caller keeps both |
What Cannot Be Overloaded
| Operator | Reason |
|---|---|
= | No backing trait |
&& | Short-circuiting, not trait-backed |
|| | Short-circuiting, not trait-backed |
| New operators | No mechanism |
Best Practices
✅ Do This:
// Implement Add for a local type
impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { ... } }
// Implement reference forms for non-Copy types
impl Add<&Label> for &Label { type Output = Label; fn add(self, other: &Label) -> Label { ... } }
// Use explicit Rhs for mixed-type operations
impl Add<Millimeters> for Meters { type Output = Millimeters; ... }
// Keep the operator's meaning conventional
// + should add, not delete or perform I/O
❌ Don’t Do This:
// Overload for a foreign type
impl Add for Vec<String> { ... } // E0117: orphan rule
// Surprising side effects
impl Add for File { fn add(self, other: File) -> File { std::fs::remove_file(...); ... } }
// Only implement one direction of a symmetric operation
// If A + B works, B + A should probably work too
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Orphan rule error (E0117) | Foreign trait for foreign type | Newtype wrapper |
Operand moved after + | Owned impl consumes | Use reference impl or Clone |
&T + &T not implemented | Only owned impl exists | Add reference impl |
T + U not valid | Rhs defaults to Self | impl Add<U> for T |
| Output type unexpected | Output chosen by implementer | Document the output type |
Real-World Examples
1. Point addition
impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { ... } }
2. Mixed units
impl Add<Millimeters> for Meters { type Output = Millimeters; ... }
3. Reference addition
impl Add<&Label> for &Label { type Output = Label; ... }
4. Scalar multiplication
impl Mul<i64> for Money { type Output = Money; ... }
5. Unary negation
impl Neg for Vector { type Output = Vector; fn neg(self) -> Vector { ... } }
6. Indexing
impl Index<usize> for Matrix { type Output = f64; fn index(&self, i: usize) -> &f64 { ... } }
7. Deref for a smart pointer
impl Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { ... } }
8. Comparison
impl PartialOrd for Version { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { ... } }
9. Generic bound
fn sum<T: Add<Output = T> + Copy>(a: T, b: T) -> T { a + b }
10. Newtype for foreign type
struct Wrapper(Vec<i32>);
impl Add for Wrapper { type Output = Wrapper; fn add(self, other: Wrapper) -> Wrapper { ... } }
Visual
Operator Syntax as Method Call
┌──────────────────────────────────────────────┐
│ a + b │
│ │ │
│ ▼ │
│ std::ops::Add::add(a, b) │
│ │ │
│ ▼ │
│ The trait method is called │
│ The operator is surface syntax │
└──────────────────────────────────────────────┘
The Add Trait
┌──────────────────────────────────────────────┐
│ trait Add<Rhs = Self> { │
│ type Output; │
│ fn add(self, rhs: Rhs) -> Self::Output; │
│ } │
│ │
│ impl Add for Point │
│ → Rhs = Point, Output = Point │
│ → Point + Point = Point │
│ │
│ impl Add<Millimeters> for Meters │
│ → Rhs = Millimeters, Output = Millimeters │
│ → Meters + Millimeters = Millimeters │
└──────────────────────────────────────────────┘
Ownership Forms
┌──────────────────────────────────────────────┐
│ T + T → consumes both │
│ &T + &T → borrows both │
│ &T + T → borrows left, consumes right│
│ T + &T → consumes left, borrows right│
│ │
│ Implement the forms the caller needs │
└──────────────────────────────────────────────┘
Orphan Rule
┌──────────────────────────────────────────────┐
│ impl Add for Vec<String> │
│ → Add is foreign │
│ → Vec is foreign │
│ → E0117: orphan rule violation │
│ │
│ FIX: newtype │
│ struct Wrapper(Vec<String>); │
│ impl Add for Wrapper { ... } │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Mechanism | Traits in std::ops |
+ | Add::add |
Default Rhs | Self |
| Result type | Associated Output |
| Ownership | Trait methods consume self |
| Reference impls | Recommended for non-Copy types |
| Orphan rule | Foreign trait + foreign type = rejected |
| Newtype | Wrapper for foreign types |
&& / || | Not overloadable |
= | Not overloadable |
Key takeaways:
- Operators are trait method calls.
a + bisa.add(b). ImplementingAddfor your type is what makes+work. The syntax is surface; the trait is the mechanism . Outputis an associated type. The implementer chooses the result type, not the caller. This is whyMeters + Millimeterscan returnMillimeterswithout the caller specifying it .Rhsdefaults toSelf.impl Add for PointmeansPoint + Point.impl Add<Millimeters> for MetersmeansMeters + Millimeterswith a custom right-hand type .- Trait methods consume
self. ForCopytypes, this is transparent. For non-Copytypes, implement the operator for references to avoid forcing the caller to clone . - The orphan rule still applies. You cannot implement
AddforVec<String>because both are foreign. Wrap the foreign type in a local newtype . - The operator’s meaning should be conventional.
+should add,*should multiply. If the operation is domain-specific, a named method is clearer than a surprising operator . - Not every operator can be overloaded.
=has no trait.&&and||are short-circuiting and unsupported. New operators cannot be created .
Remember: Operator overloading in Rust is not a special language feature. It is trait implementation. The operator symbols are sugar for method calls, and the std::ops traits define the method signatures. The Add trait is the model: a default Rhs type parameter, an associated Output type, and a method that consumes its operands. The ownership model determines whether you implement the operator for owned values, references, or both. The orphan rule determines whether you can implement it for a foreign type, and the newtype pattern is the answer when you cannot. The operator’s meaning should be recognizable—+ adds, * multiplies—because the syntax is shorthand, and shorthand only works when the reader does not have to look up what it means.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!