如何在Rust中实现对值和引用的惯用运算符重载?

17

当实现一个基本的固定大小向量类型(例如float2)时,我想支持AddSub特性。稍后,我还想支持Mul*Assign

查阅文档和其他例子后,我得到了以下结果:

use std::ops::{Add, Sub};

#[derive(Copy, Clone)]
struct float2(f64, f64);

impl Add for float2 {
    type Output = float2;
    fn add(self, _rhs: float2) -> float2 {
        float2(self.0 + _rhs.0, self.1 + _rhs.1)
    }
}

impl Sub for float2 {
    type Output = float2;
    fn sub(self, _rhs: float2) -> float2 {
        float2(self.0 - _rhs.0, self.1 - _rhs.1)
    }
}

这对于基本示例有效,但是我在实践中发现我经常会将引用作为参数传递以及栈上的本地float2

为了混合这些,我需要:

  • 取消引用变量(可以,但使代码略微难以阅读)。
  • 声明操作符重载组合的引用。

示例:

impl<'a, 'b> Add<&'b float2> for &'a float2 {
    type Output = float2;
    fn add(self, _rhs: &'b float2) -> float2 {
        float2(self.0 + _rhs.0, self.1 + _rhs.1)
    }
}
impl<'a> Add<float2> for &'a float2 {
    type Output = float2;
    fn add(self, _rhs: float2) -> float2 {
        float2(self.0 + _rhs.0, self.1 + _rhs.1)
    }
}
impl<'b> Add<&'b float2> for float2 {
    type Output = float2;
    fn add(self, _rhs: &'b float2) -> float2 {
        float2(self.0 + _rhs.0, self.1 + _rhs.1)
    }
}

/*... and again for Sub */

虽然这样可以编写没有解引用的表达式,但是在列举每个组合时变得非常繁琐,特别是添加更多操作和类型(float3float4等)。

是否有一种普遍接受的方法来自动强制转换运算符重载的类型?

使用宏或语言的其他功能来避免繁琐的重复操作?

还是开发人员需要明确地根据需要访问变量作为引用或解引用,并编写大量重复的运算符重载函数?

注意,我目前是初学者,我检查了一些相当高级的Rust数学库,它们超出了我的水平,虽然我可以使用它们 - 但是我想知道如何为自己的类型编写运算符重载。

2个回答

13
Rust的优点在于它是开源的。这意味着你可以看到语言作者是如何解决问题的。最接近的类比是原始整数类型
macro_rules! add_impl {
    ($($t:ty)*) => ($(
        #[stable(feature = "rust1", since = "1.0.0")]
        impl Add for $t {
            type Output = $t;

            #[inline]
            fn add(self, other: $t) -> $t { self + other }
        }

        forward_ref_binop! { impl Add, add for $t, $t }
    )*)
}

forward_ref_binop被定义为:

macro_rules! forward_ref_binop {
    (impl $imp:ident, $method:ident for $t:ty, $u:ty) => {
        #[stable(feature = "rust1", since = "1.0.0")]
        impl<'a> $imp<$u> for &'a $t {
            type Output = <$t as $imp<$u>>::Output;

            #[inline]
            fn $method(self, other: $u) -> <$t as $imp<$u>>::Output {
                $imp::$method(*self, other)
            }
        }

        #[stable(feature = "rust1", since = "1.0.0")]
        impl<'a> $imp<&'a $u> for $t {
            type Output = <$t as $imp<$u>>::Output;

            #[inline]
            fn $method(self, other: &'a $u) -> <$t as $imp<$u>>::Output {
                $imp::$method(self, *other)
            }
        }

        #[stable(feature = "rust1", since = "1.0.0")]
        impl<'a, 'b> $imp<&'a $u> for &'b $t {
            type Output = <$t as $imp<$u>>::Output;

            #[inline]
            fn $method(self, other: &'a $u) -> <$t as $imp<$u>>::Output {
                $imp::$method(*self, *other)
            }
        }
    }
}

编写包装实现特征的引用是完全有效的,这些包装只需取消引用并调用面向值的版本。


一个人可以从std::ops导入forward_ref_binop吗?还是必须重新定义宏? - Aratz
1
@Aratz,你需要重新定义它。 - Shepmaster
非常好的答案!使用类似于这样但是“反转”的方法(实现ref-ref,并让宏来处理其余部分)对我来说效果很好。 - Uncle Dino
2
箱子 forward_ref 提供了 forward_ref_binopforward_ref_op_assignforward_ref_unop 宏,虽然它们与核心中的宏相比较简化。 - RedGlyph

3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接