Arithmetic and Bitwise Operators

How to overload arithmetic and bitwise operators for custom types in Rust using the std::ops traits Add Sub Mul Div Rem BitAnd BitOr BitXor Shl and Shr

Rust’s +, -, *, /, %, &, |, ^, <<, and >> operators are not built into the language as special syntax. They are syntactic sugar for method calls on traits from std::ops. When you write a + b, the compiler desugars it into a.add(b), where add is a method from the Add trait. This design means you can make your own types work with these operators by implementing the right trait. That is operator overloading in Rust.

This page covers the traits that govern arithmetic and bitwise binary operators. Unary operators (-, !), compound assignment (+=, &=, etc.), and comparison operators are separate traits and are discussed in their own sections.

Only traits can be overloaded:

Rust does not allow creating entirely new operators or overloading operators that lack a corresponding trait. The assignment operator = has no trait, so you cannot change what assignment does. If you need custom syntax, macros are the intended extension mechanism.

The arithmetic and bitwise operator traits

Each binary arithmetic or bitwise operator maps to a trait with an associated method. The traits live in std::ops and share a common shape: a generic parameter Rhs that defaults to Self, an associated type Output, and a method that consumes self and the right-hand side to produce an Output.

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

All of Rust’s built-in numeric types implement these traits for same-type operations, and many also accept references. That is why 5 + 3 works. When you define your own numeric-like type, you decide which of these traits to implement to make it feel natural to use.

& and | are bitwise, not logical:

The & and | operators overload the bitwise AND and OR, not the short-circuiting logical && and ||. Those logical operators cannot be overloaded because they rely on lazy evaluation semantics that the trait system does not model.

Understanding Add as the blueprint

Every binary operator trait follows the same pattern, so learning Add deeply gives you the pattern for the rest. Here is the definition of std::ops::Add:

pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}
  • Rhs is the type of the right-hand operand. It defaults to Self, so Add without a type parameter means “addition with another value of the same type.”
  • Output is the type the operation produces. It does not have to be Self. You could add two Point values and get a Vector, or add a Duration to a Time and get a new Time.
  • add takes ownership of both self and rhs. This is the simplest version; you can also implement the trait for references to avoid moving the original values.

If you only need to support a + b where a and b are the same concrete type, you write impl Add for MyType { type Output = MyType; ... }. The default Rhs = Self handles the rest.

A first implementation: adding 2D points

Suppose you have a Point struct that represents coordinates in space. Adding two points should produce a new point whose coordinates are the sums of the components.

use std::ops::Add;
#[derive(Debug, PartialEq)]
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 a = Point { x: 1, y: 2 };
    let b = Point { x: 3, y: 4 };
    assert_eq!(a + b, Point { x: 4, y: 6 });
}

This code compiles and runs. After the impl Add for Point, the + operator is available for Point. The call a + b becomes a.add(b). Because add takes self by value, the original a and b are moved into the function and can no longer be used afterward.

Do not call the operator inside its own implementation:

A common mistake is to write self.x + rhs.x while implementing Add, assuming it calls the underlying integer addition. That is correct because i32 has its own Add implementation. But if you accidentally write self + rhs inside fn add, you trigger infinite recursion: a.add(b) calls itself until the stack overflows. Always decompose into the fields or use a different method.

Once the implementation compiles and passes a basic assertion like the one above, you have successfully overloaded + for your type.

If your assertion passes, it works:

Seeing assert_eq!(a + b, expected) succeed confirms that the trait is wired correctly and the logic behaves as intended. You can add more tests for edge cases, but this minimal check proves the operator is functional.

Generic implementations to avoid repetition

Implementing Add separately for Point<i32>, Point<f64>, and Point<usize> would be tedious and repetitive. Instead, you can write one generic implementation that works for any component type that already supports addition.

use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point<T> {
    x: T,
    y: T,
}
impl<T: Add<Output = T>> Add for Point<T> {
    type Output = Point<T>;
    fn add(self, rhs: Point<T>) -> Point<T> {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

The bound T: Add<Output = T> ensures that the component type T can be added to itself and produces the same type. That covers integers, floats, and any custom numeric type that satisfies the constraint.

You can loosen the bound further:

Rust allows the left and right operands of + to have different types, and the output does not have to match either. A maximally generic implementation could look like impl<L, R> Add<Complex<R>> for Complex<L> where L: Add<R>, allowing mixed-type addition. In practice, Rust’s standard library avoids mixed-type arithmetic for numeric types, and most custom types follow the same convention. The simpler same-type bound is almost always sufficient.

Preserving operands: implementing Add for references

If your type is not Copy, implementing Add directly on the type means the operands are moved and consumed. That is acceptable if you never need the original values again, but for types that are used in multiple expressions, you usually want the operands to remain usable. The solution is to implement Add for references.

impl<'a, 'b> Add<&'b Point> for &'a Point {
    type Output = Point;
    fn add(self, rhs: &'b Point) -> Point {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

Now you can write let c = &a + &b; and both a and b remain accessible. The trade-off is that the call site requires explicit references. There is no way to make a + b work without moving or copying the operands if you implement Add on the value type. Rust does not automatically insert references for operator operands.

If your type is small and trivially copiable (like a struct of a few integers), the idiomatic path is to derive Copy and implement Add by value. The performance cost of copying 24 or 48 bytes is negligible in nearly all contexts, and the compiler often optimizes the copies away.

Consider implementing both value and reference overloads:

A common pattern is to implement Add for the value type when it is Copy, and additionally implement Add<&T> and &T + T etc. for generic code that may hold references. This makes your type flexible without forcing callers to write & everywhere.

Subtraction, multiplication, division, and remainder

The traits Sub, Mul, Div, and Rem follow the exact same shape as Add. Each has a default Rhs = Self, an associated Output, and a method (sub, mul, div, rem) that consumes the operands. The implementation patterns are identical; you replace the body logic with the corresponding operation.

use std::ops::{Sub, Mul, Div, Rem};
impl Sub for Point {
    type Output = Point;
    fn sub(self, rhs: Point) -> Point {
        Point { x: self.x - rhs.x, y: self.y - rhs.y }
    }
}
impl Mul for Point {
    type Output = Point;
    fn mul(self, rhs: Point) -> Point {
        Point { x: self.x * rhs.x, y: self.y * rhs.y }
    }
}
impl Div for Point {
    type Output = Point;
    fn div(self, rhs: Point) -> Point {
        Point { x: self.x / rhs.x, y: self.y / rhs.y }
    }
}
impl Rem for Point {
    type Output = Point;
    fn rem(self, rhs: Point) -> Point {
        Point { x: self.x % rhs.x, y: self.y % rhs.y }
    }
}

Each of these can also be made generic by adding the appropriate trait bound on the component type, e.g., T: Mul<Output = T>.

Division and remainder on integers panic on zero:

If you implement Div or Rem for a type containing integers, be aware that the underlying integer operation will panic at runtime when the right-hand operand is zero. For production libraries, consider documenting this behavior or returning a Result-based alternative method for cases where zero might appear.

Bitwise operators for custom types

The bitwise traits BitAnd, BitOr, BitXor, Shl, and Shr are used for bit-manipulation operations. They are implemented by Rust’s integer types and bool, but you can implement them for your own types to create expressive APIs—for example, combining bitflags, manipulating permissions, or working with packed data structures.

use std::ops::BitAnd;
#[derive(Debug, PartialEq, Clone, Copy)]
struct Permissions(u8);
impl BitAnd for Permissions {
    type Output = Permissions;
    fn bitand(self, rhs: Permissions) -> Permissions {
        Permissions(self.0 & rhs.0)
    }
}
fn main() {
    let read = Permissions(0b001);
    let write = Permissions(0b010);
    let read_write = read | write; // requires BitOr impl
    let intersection = read & read_write;
    assert_eq!(intersection, read);
}

For bitwise shifts, the right-hand side does not have to be the same type. The Shl and Shr traits are generic over the right-hand operand, and the standard library implements them for integers with a variety of right-hand types (u8, u16, etc.). When implementing shift for a custom type, you can choose the appropriate Rhs.

Shift operations on integers have panicking behavior:

In Rust, shifting by a number of bits greater than or equal to the bit width of the type will panic in debug builds and wrap in release builds for integers. If you implement Shl or Shr for a custom type that delegates to integer shifts, the same rules apply. Document the behavior or clamp the shift amount if that is unacceptable for your domain.

Common mistakes when overloading arithmetic and bitwise operators

Several pitfalls catch beginners repeatedly. Knowing them in advance saves debugging time.

Forgetting to import the trait. The Add, Sub, etc. traits must be in scope for the operator to work. Without use std::ops::Add;, the compiler will not know that + is available for your type, even if the impl block exists in the same file.

Calling the operator inside its own implementation. Using self + rhs inside fn add causes infinite recursion because + is desugared to self.add(rhs). Always decompose into the primitive operations on the fields.

Assuming AddAssign is automatic. Implementing Add does not give you +=. The compound assignment operator += is controlled by the separate AddAssign trait. You need to implement that explicitly if you want x += y to work.

Not considering ownership. The default add method takes self by value, moving the left operand. If your type is not Copy and you want to keep using the original value, implement Add for references or derive Copy if the type is lightweight.

Forgetting to derive required traits for testing. Assertions like assert_eq! require PartialEq and Debug. When testing your operator implementation, add #[derive(Debug, PartialEq)] to your struct to get meaningful error messages and comparisons.

Missing imports cause opaque errors:

If you write impl Add for MyType but forget use std::ops::Add;, the compiler error may mention that Add is not found in the current scope. The fix is a single import. Always check that the trait name is recognized before debugging the implementation.

Where these traits appear in real Rust code

Arithmetic and bitwise operator overloading is widely used in libraries that define mathematical types:

  • Linear algebra crates (nalgebra, cgmath) implement Add, Sub, Mul for vector and matrix types so that expressions like a * b + c look like mathematical notation.
  • Date and time libraries (chrono, time) implement Add<Duration> on date types to support today + 5.days().
  • Bitflag crates (bitflags) generate types that implement bitwise operators to combine and test flags with | and &.
  • Custom numeric types (fixed-point, rational, complex) rely on these traits to integrate with generic algorithms that accept T: Add<Output = T> bounds.

Understanding the pattern behind Add and its siblings equips you to write types that feel like first-class numeric citizens in Rust.

Summary

Rust’s arithmetic and bitwise operators—+, -, *, /, %, &, |, ^, <<, >>—are not magic. They are method calls on the traits Add, Sub, Mul, Div, Rem, BitAnd, BitOr, BitXor, Shl, and Shr. Each trait follows a consistent structure: a defaulted right-hand type parameter, an associated Output type, and a method that takes ownership of the operands. Implementing any of these traits enables the corresponding operator for your type, and generic implementations let you cover an entire family of component types with a single block.

The key design choice is whether to implement the trait for the value type itself (which moves the operands) or for references (which preserves them). For small Copy types, implementing by value is idiomatic and performant; for larger or non-Copy types, reference implementations keep the original values alive. You can provide both for maximum flexibility.