RustArena

Step 4 of 7

Operator overloading

Rust's operators are traits in std::ops. a + b is really Add::add(a, b), so implementing Add for your type makes + work on it. This is called operator overloading. It's perfect for math-like types: vectors, money, matrices, durations.

use std::ops::{Add, AddAssign, Mul};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vector2 {
    x: f64,
    y: f64,
}

impl Add for Vector2 {
    type Output = Vector2;

    fn add(self, other: Vector2) -> Vector2 {
        Vector2 { x: self.x + other.x, y: self.y + other.y }
    }
}

impl Mul<f64> for Vector2 {
    type Output = Vector2;

    fn mul(self, k: f64) -> Vector2 {
        Vector2 { x: self.x * k, y: self.y * k }
    }
}

impl AddAssign for Vector2 {
    fn add_assign(&mut self, other: Vector2) {
        self.x += other.x;
        self.y += other.y;
    }
}

fn main() {
    let mut pos = Vector2 { x: 1.0, y: 2.0 };
    let vel = Vector2 { x: 0.5, y: -1.0 };
    println!("{:?}", pos + vel);
    println!("{:?}", vel * 4.0);
    pos += vel;
    pos += vel;
    println!("{:?}", pos);
}
Vector2 { x: 1.5, y: 1.0 }
Vector2 { x: 2.0, y: -4.0 }
Vector2 { x: 2.0, y: 0.0 }

Piece by piece:

Operator Trait Operator Trait
a + b Add a += b AddAssign
a - b Sub a -= b SubAssign
a * b Mul a / b Div
-a Neg !a Not
a == b PartialEq a < b PartialOrd
a[i] Index a[i] = x IndexMut

Neg is the unary minus: fn neg(self) -> Self::Output, with only one operand.

Index: square brackets on your own types

Index makes value[i] work. It returns a reference to something inside:

use std::ops::Index;

struct Playlist {
    songs: Vec<String>,
}

impl Index<usize> for Playlist {
    type Output = String;

    fn index(&self, i: usize) -> &String {
        &self.songs[i]
    }
}

fn main() {
    let p = Playlist { songs: vec![String::from("Crab Rave"), String::from("Ferris Wheel")] };
    println!("{}", p[1]);
}
Ferris Wheel

The index type doesn't have to be a number: HashMap implements Index<&K>, so scores["ana"] works (and panics if the key is missing, like an out-of-bounds index).

Workplace note

Overload an operator only when its meaning is obvious to every reader: + on money or vectors, yes; + to "add a user to a group", no (write a method). Teams usually implement the full set together (Add, Sub, AddAssign...), so the type isn't surprising to use.

Common mistakes

use std::ops::Mul;

#[derive(Debug, Clone, Copy)]
struct Vector2 {
    x: f64,
    y: f64,
}

impl Mul<f64> for Vector2 {
    type Output = Vector2;

    fn mul(self, k: f64) -> Vector2 {
        Vector2 { x: self.x * k, y: self.y * k }
    }
}

fn main() {
    let v = Vector2 { x: 1.0, y: 2.0 };
    println!("{:?}", 2.0 * v);
}
error[E0277]: cannot multiply `{float}` by `Vector2`

For that you'd also write impl Mul<Vector2> for f64.

Your task

Add is already done. Implement Sub (a - b) and Neg (-a) for Vector2.

Challenge 2

Make Money (a whole number of cents) work with +, += and * by a quantity. Implement Add (Money + Money), AddAssign (total += price) and Mul<i64> (Money * quantity).

Challenge 3

A game board stores its cells in one Vec<char>, row after row. Implement Index<(usize, usize)> for Grid so grid[(row, col)] returns the cell at that row and column. The cell's position in cells is row * width + col.

Previous: FromStr: parse into your own type Next: Ordering: PartialOrd and Ord for sorting