Operator Overloading

Go here for references.

Arithmetic and Bitwise Operators

Here's the definition of std::ops::Add:


#![allow(unused)]
fn main() {
trait Add<RHS=Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}
}

In other words, the trait Add<T> is the ability to add a T value to yourself.

We could implement Add generically for the Complex number type like this:


#![allow(unused)]
fn main() {
use std::ops::Add;

impl<T> Add for Complex<T> where T: Add<Output=T> {
    type Output = Self;
    fn add(self, rhs: Self) -> Self {
        Complex { re: self.re + rhs.re, im: self.im + rhs.im }
    }
}
}

Unary Operators

The two overloadable unary operators (! and -) are defined like this:


#![allow(unused)]
fn main() {
trait Not {
    type Output;
    fn not(self) -> Self::Output;
}

trait Neg {
    type Output;
    fn neg(self) -> Self::Output;
}
}

An implementation of Neg for Complex values might look like this:


#![allow(unused)]
fn main() {
impl<T, O> Neg for Complex<T> where T: Neg<Output=O> {
    type Output = Complex<O>;
    fn neg(self) -> Complex<O> {
        Complex { re: -self.re, im: -self.im }
    }
}
}

Binary Operators

The definition of std::ops::BitXor looks like this:


#![allow(unused)]
fn main() {
trait BitXor<RHS=Self> {
    type Output;
    fn bitxor(self, rhs: RHS) -> Self::Output;
}
}

Compound Assignment Operators

Warning: Unlike other languages, the value of a compound assignent expression is always (). e.g. x += y returns ().

The definition of std::ops::AddAssign looks like this:


#![allow(unused)]
fn main() {
trait AddAssign<RHS=Self> {
    fn add_assign(&mut self, RHS);
}
}

An implementation of AddAssign for Complex values might look like this:


#![allow(unused)]
fn main() {
impl<T> AddAssign for Complex<T> where T: AddAssign<T> {
    fn add_assign(&mut self, rhs: Complex<T>) {
        self.re += rhs.re;
        self.im += rhs.im;
    }
}
}

Warning: Overloading an arithmetic operator like Add does not automatically include overload implementation for its corresponding AddAssign operator.

Equality Tests

Since the ne method of the PartialEq trait already has a default implementation, you'll only ever need to implement the eq method.

PartialEq takes its values by reference.

Here's the definition of std::cmp::PartialEq:


#![allow(unused)]
fn main() {
trait PartialEq<Rhs: ?Sized = Self> {
    fn eq(&self, other: &Rhs) -> bool;
    // `ne` has a default implementation
    fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}
}

Syntax: The where Rhs: ?Sized bound relaxxs Rust's usual requirement that type parameters must be sized types, which lets us write traits like PartialEq<str> or PartialEq<[T]>.

Tip: In most cases, Rust can automatically implement PartialEq for your type for you if you add #[Derive(PartialEq)].

Ordered Comparisons

Ordered comparison operators all stem from the std::cmp::PartialOrd trait, which is defined as:


#![allow(unused)]
fn main() {
trait PartialOrd<Rhs = Self>: PartialEq<Rhs> where Rhs: ?Sized {
    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 { ... }
}
}

Note that PartialOrd is a subtrait of PartialEq. Meaning you can perform ordered comparison only on types that can also be checked for equality.

Also note that partial_cmp is the only method of the PartialOrd trait that doesn't have a default implementation. This means when you want to implement PartialOrd, you only need to define partial_cmp.

Index and IndexMut

Here are the definitions of the traits associated with the index operator:


#![allow(unused)]
fn main() {
trait Index<Idx> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

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

The associated type Output specifies what an index expression returns.

Use Case: The most common use case for indexing and overloading the index operators is for collections.

In the Mandelbrot program, we accessed pixels with lines like this:


#![allow(unused)]
fn main() {
// current implementation treats pixels as a single row
pixels[row * bounds.0 + column] = ...; // UGLY
// what we want is to be able to access pixels as if it were a 2D array
image[row][column] = ...; // BETTER!
}

To achieve improved indexing in the above code, we could write something like this:


#![allow(unused)]
fn main() {
// declare a struct that holds the pixels and the image dimensions
#[derive(Debug)]
struct Image<P> {
    width: usize,
    pixels: Vec<P>,
}

// add a static constructor to the Image type
// the type parameter P is the pixel type
impl<P> Image<P> where P: Default + Copy {
    fn new(width: usize, height: usize) -> Image<P> {
        Image {
            width,
            pixels: vec![P::default(); width * height],
        }
    }

    fn height(&self) -> usize {
        self.pixels.len() / self.width
    }
}

// now we implement Index and IndexMut
// when we index into an Image<P>, we expect to get back a slice of P
// indexing the slice will give an individual pixel
impl<P> std::ops::Index<usize> for Image<P> {
    type Output = [P];
    fn index(&self, row: usize) -> &Self::Output {
        let start = row * self.width;
        &self.pixels[start .. start + self.width]
    }
}

impl<P> std::ops::IndexMut<usize> for Image<P> {
    fn index_mut(&mut self, row: usize) -> &mut [P] {
        let start = row * self.width;
        &mut self.pixels[start .. start + self.width]
    }
}

// Create an image 3 pixels wide and 3 pixels tall
let mut image = Image::<u32>::new(3, 3);
println!("image height {}", image.height());

// Draw a diagonal line through the image
for i in 0 .. image.width {
    image[i][i] = 255;
}
println!("{:?}", image);
}

Other Operators

The dereferencing operator (*val) and the dot operator for accessing fields and calling methods (val.field and val.method()), can be overloaded using the Deref and DerefMut traits.