Any lib or macros that ease operator overloading

I am doing my CG homework using Rust. When it comes to overloading operators, it seems I have to write really verbose code to get it usable.

Say, I have a pub struct Vec3, and I want an element-wise addition and a broadcast addition. I have to write the below, so that + can work with references.

#[derive(Debug, Copy, Clone)]
pub struct Vec3
{
    data: [f32; 3]
}

impl Vec3
{
    pub fn new(x: f32, y: f32, z: f32) -> Self
    {
        Vec3 {
            data: [x, y, z]
        }
    }
}

// impl ops::Add for Vec3
// {
//     type Output = Vec3;
//
//     fn add(self, rhs: Self) -> Self::Output {
//         let data = [self.data[0] + rhs.data[0], self.data[1] + rhs.data[1], self.data[2] + rhs.data[2]];
//         Vec3 {
//             data
//         }
//     }
// }

// impl ops::Add<Vec3> for &Vec3
// {
//     type Output = Vec3;
//
//     fn add(self, rhs: Vec3) -> Self::Output {
//         let data = [self.data[0] + rhs.data[0], self.data[1] + rhs.data[1], self.data[2] + rhs.data[2]];
//         Vec3 {
//             data
//         }
//     }
// }

impl ops::Add for &Vec3
{
    type Output = Vec3;

    fn add(self, rhs: Self) -> Self::Output {
        let data = [self.data[0] + rhs.data[0], self.data[1] + rhs.data[1], self.data[2] + rhs.data[2]];
        Vec3 {
            data
        }
    }
}


impl ops::Add<&Self> for Vec3
{
    type Output = Vec3;

    fn add(self, rhs: &Self) -> Self::Output {
        let data = [self.data[0] + rhs.data[0], self.data[1] + rhs.data[1], self.data[2] + rhs.data[2]];
        Vec3 {
            data
        }
    }
}

// broadcasting
impl ops::Add<f32> for Vec3
{
    type Output = Vec3;

    fn add(self, rhs: f32) -> Self::Output {
        let data = [self.data[0] + rhs, self.data[1] + rhs, self.data[2] + rhs];
        Vec3 {
            data
        }
    }
}

impl ops::Add<f32> for &Vec3
{
    type Output = Vec3;

    fn add(self, rhs: f32) -> Self::Output {
        let data = [self.data[0] + rhs, self.data[1] + rhs, self.data[2] + rhs];
        Vec3 {
            data
        }
    }
}

I understand that the ownership matters, so I commented codes in the middle to avoid taking the ownership, but I think the rest is basically the same (conceptually). Is there any macros or ways to overload operators regardless whether the left hand side is a reference or a value?

Since the below lines should be equivalent, it makes sense to ignore whether the left hand side is a reference.

let val = Vec3::new(1.0,1.0,1.0);
let reference = &val;
let result1 = val + 1.0;
let result2 = val.add(1.0);
let result3 = reference.add(1.0);
// so why not like below?
let result4 = reference + 1.0; // my code compiles, but I have to explicitly `impl ops::Add<f32> for &Vec3`

I tried the below code using blanket implementation, but it cannot compile due to the orphan rule.

 impl<T: Add<f32>> Add<f32> for &T
 {
     type Output = T::Output;

     fn add(self, rhs: f32) -> Self::Output {
         self.add(rhs)
     }
 }

A couple of pointers:

  1. That code could never work, as the add() method simply recurses. It cannot do anything other than cause a stack overflow.

  2. The best that can be done here is to write some kind of macro (I'm not convinced macro_rules macros are powerful enough) that takes as arguments the syntactic bits and pieces that differ between the impls. But this macro would be somewhat complicated to write if you want to support overriding the RHS type variable like it is in a regular std::ops trait impl.

  3. You can work around the orphan rules by using a newtype that wraps f32, say struct F32(f32); and optionally implement std::ops::Deref<Target = f32> for it.
    Armed with that newtype, you can write

impl Add<F32> for F32 {
     type Output = T::Output;

     fn add(self, rhs: F32) -> Self::Output {
         self.0 + rhs.0
     }
 }

Because Vec3 is Copy and quite small, you don’t need to worry about ownership nearly as much, especially when there’s no mutation involved. You can then write your reference add implementation like this, which is generic over the right-hand side:

impl<T> ops::Add<T> for &Vec3 where Vec3:Add<T>
{
    type Output = <Vec3 as Add<T>>::Output;

    fn add(self, rhs: T) -> Self::Output {
        (*self) + rhs
    }
}

Because of the orphan rules, you can’t make a blanket implementation that might define Add for any types outside the crate. The pattern above can be turned into a macro without too much trouble, but it looks quite daunting if written to support generic types:

(I use Clone instead of copy, so it works for a wider range of types)


macro_rules! impl_ref_add {
    ($T:ident $(< $($lt:lifetime,)* $($arg:ident),*>)?) => {
         impl<$($($lt,)* $($arg,)*)? Rhs> Add<Rhs> for
         &$T$(<$($lt,)* $($arg,)* >)?
         where $T$(<$($lt,)* $($arg,)* >)? : Clone + Add<Rhs> {
             type Output = < $T$(<$($lt,)* $($arg,)* >)? as Add<Rhs>>::Output;
             fn add(self, rhs:Rhs)->Self::Output {
                 self.clone() + rhs
             }
         }
    }
}

(Playground)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.