rustoperator-overloadingborrow-checkerarithmetic-expressions

How to implement operators for a Rust struct when the operands might be values or references?


I am new to this, and trying to overload enough operators (Mul, Sub) to get a simple arithmetic function on a simple type to compile:

#[derive(Debug)]
struct Tuple {
    x: f64, y: f64, z: f64, w: f64,
}

fn dot(a: &Tuple, b: &Tuple) -> f64 {
    (a.x * b.x) + (a.y * b.y) + (a.z * b.z) + (a.w * b.w)
}


impl std::ops::Mul<f64> for Tuple {
    type Output = Tuple;
    fn mul(self, rhs: f64) -> Tuple {
        Tuple{x: self.x * rhs, y: self.y * rhs,
              z: self.z * rhs, w: self.w * rhs}
    }
}

impl std::ops::Mul<f64> for &Tuple {
    type Output = Tuple;
    fn mul(self, rhs: f64) -> Tuple {
        Tuple{x: self.x * rhs, y: self.y * rhs,
              z: self.z * rhs, w: self.w * rhs}
    }
}

impl std::ops::Sub<&Tuple> for Tuple {
    type Output = Tuple;
    fn sub(self, rhs: &Tuple) -> Tuple {
        Tuple{x: self.x - rhs.x, y: self.y - rhs.y,
              z: self.z - rhs.z, w: self.w - rhs.w}
    }
}

impl std::ops::Sub<&Tuple> for &Tuple {
    type Output = Tuple;
    fn sub(self, rhs: &Tuple) -> Tuple {
        Tuple{x: self.x - rhs.x, y: self.y - rhs.y,
              z: self.z - rhs.z, w: self.w - rhs.w}
    }
}

fn reflect(inc: &Tuple, normal: &Tuple) -> Tuple {
    //inc - normal * 2.0 * inc.dot(&normal)
    let a = dot(&inc, &normal);
    let b = 2.0 * a;
    let c = normal * b;
    inc - c    // <--- compile error: expected `&Tuple`, found struct `Tuple`
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn something() {
        let v = Tuple { x: 1.0, y: -1.0, z: 0.0, w: 0.0 };
        let n = Tuple { x: 0.0, y: 1.0, z: 0.0, w: 0.0 };

        //let r = reflect(&v, &n);
        let r = v - n * 2.0 * dot(&v, &n);  // <-- compile error: expected `&Tuple`, found struct `Tuple`
//                  ~~~~~~~~~~~~~~~~~~~~~        

        println!("{:?}", r);
    }
}

Rust Playground

The problem is that I just can't seem to get my head around all the different implementations that I need to define - there seems to be separate impl functions required for not just values but also references to values. And once I do implement enough of them, I seem to end up with a reference instead of a value:

error[E0308]: mismatched types
  --> lib/tuple.rs:70:11
   |
70 |     inc - c
   |           ^
   |           |
   |           expected `&Tuple`, found struct `Tuple`
   |           help: consider borrowing here: `&c`

I'm perplexed as to why I can't write something this simple - what am I missing?

More worryingly, if I do get this to work, do I really need to implement a huge number of functions for not just each operator, but each combination?

Can a reference be automatically dereferenced into a value, and the value version of the operator function used? They are references to immutable values after all - the values are right there.

(I wasn't sure whether to mention this, but I was actually using use derive_more::{Mul}; on a newtype, wrapping another tuple type, but derive_more only seems to include support for automatic generation of the both-are-values binary operator functions, not the other three involving one or two references, so in this question I've reverted back to a basic struct where I see the same sort of problem).


Solution

  • One way to handle this would be to use macros, which is something the standard library does quite often for cases similar to yours.

    In your case, you could do, e.g.:

    macro_rules! tuple_mul {
        ( $lhs:ty , $rhs:ty ) => {
            impl std::ops::Mul<$rhs> for $lhs {
                type Output = Tuple;
                fn mul(self, rhs: $rhs) -> Tuple {
                    Tuple{x: self.x * rhs, y: self.y * rhs,
                          z: self.z * rhs, w: self.w * rhs}
                }
            }
        }
    }
    
    tuple_mul!(Tuple, f64);
    tuple_mul!(Tuple, &f64);
    tuple_mul!(&Tuple, f64);
    tuple_mul!(&Tuple, &f64);
    

    And similar for other operators. This way you can avoid copy-pasting the redundant code, instead using macros to generate it for you and make your code more maintainable.