Associated Types and Advanced Traits

Implementing Other Operator Traits

Beyond Add, the std::ops module provides traits for compound assignment (AddAssign, SubAssign), indexing (Index, IndexMut), unary operations (Neg, Not), and bitwise operations (BitAnd, BitOr, BitXor, Shl, Shr).

Compound Assignment Traits (AddAssign, SubAssign, MulAssign)

Implementing Add (+) does not automatically enable +=. Compound assignment operators mutate the left-hand operand in place via &mut self and return ():

pub trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, rhs: Rhs);
}
use std::ops::{AddAssign, MulAssign};

#[derive(Debug, Clone, Copy)]
struct Counter(i64);

impl AddAssign<i64> for Counter {
    fn add_assign(&mut self, delta: i64) {
        self.0 += delta;
    }
}

impl MulAssign<i64> for Counter {
    fn mul_assign(&mut self, factor: i64) {
        self.0 *= factor;
    }
}

fn main() {
    let mut c = Counter(10);
    c += 5;
    c *= 3;
    println!("Counter: {c:?}");
}
Counter: Counter(45)

Indexing Traits (Index and IndexMut)

The Index<Idx> and IndexMut<Idx> traits overload the container[index] syntax for shared and mutable access:

pub trait Index<Idx: ?Sized> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

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

Note that index returns a reference &Self::Output, which the compiler automatically dereferences at the call site (container[i] desugars to *container.index(i)):

use std::ops::{Index, IndexMut};

struct Grid2D {
    width: usize,
    cells: Vec<i32>,
}

impl Grid2D {
    fn new(width: usize, height: usize) -> Self {
        Self {
            width,
            cells: vec![0; width * height],
        }
    }
}

impl Index<(usize, usize)> for Grid2D {
    type Output = i32;

    fn index(&self, (col, row): (usize, usize)) -> &Self::Output {
        &self.cells[row * self.width + col]
    }
}

impl IndexMut<(usize, usize)> for Grid2D {
    fn index_mut(&mut self, (col, row): (usize, usize)) -> &mut Self::Output {
        &mut self.cells[row * self.width + col]
    }
}

fn main() {
    let mut grid = Grid2D::new(4, 4);
    grid[(2, 1)] = 99; // Calls IndexMut::index_mut
    println!("Cell (2, 1) = {}", grid[(2, 1)]); // Calls Index::index
}
Cell (2, 1) = 99

Bitwise Flag Combination (BitOr and BitAnd)

Bitflag types commonly implement BitOr (|), BitAnd (&), and Not (!) so permissions or flags can be combined and tested ergonomically:

use std::ops::{BitAnd, BitOr};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Permissions(u8);

impl Permissions {
    const READ: Self = Self(0b001);
    const WRITE: Self = Self(0b010);
    const EXEC: Self = Self(0b100);

    fn contains(self, other: Self) -> bool {
        (self & other) == other
    }
}

impl BitOr for Permissions {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

impl BitAnd for Permissions {
    type Output = Self;
    fn bitand(self, rhs: Self) -> Self {
        Self(self.0 & rhs.0)
    }
}

fn main() {
    let rw = Permissions::READ | Permissions::WRITE;
    println!("Has READ: {}", rw.contains(Permissions::READ));
    println!("Has EXEC: {}", rw.contains(Permissions::EXEC));
}
Has READ: true
Has EXEC: false