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 correspondingAddAssign
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 likePartialEq<str>
orPartialEq<[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.