#operator #operator-overloading #overload #binary-operator #macro

inertia-macros

一个简单的属性宏库,用于协助对借用和所有权的运算符进行重载

1 个不稳定版本

0.1.0 2021年10月22日

#725过程宏

Apache-2.0

24KB
422

opimps

opimps 简化了Rust的运算符重载,使其可以以类似于C++的方式编写,但无需重复代码。

总结

在Rust中重载运算符时,我们可能会遇到设计问题,即数据应该是 borrowedowned。在许多情况下,我们并不关心这一点,应该由运算符的调用者来决定使用什么。

在下面的示例中,我们重载了二元运算符 +,以便计算两个车库中的汽车总数。

想象我们有一个车库,里面存放了一些汽车。

struct Garage {
    number_of_cars: u64
}

使用 opimps,我们可以重载运算符,以便执行类似在两个车库之间添加汽车数量这样的操作。

use core::ops::Add;

#[opimps::impl_ops(Add)]
fn add(self: Garage, rhs: Garage) -> u64 {
    self.number_of_cars + rhs.number_of_cars
}

在幕后,代码会生成以下代码,如果我们想要允许所有权和借用数据的组合,我们原本需要手动实现这些代码。

use core::ops::Add;

struct Garage {
    number_of_cars: u64
}

impl Add for Garage {
    type Output = u64;
    fn add(self, rhs: Garage) j-> Self::Output {
        self.number_of_cars + rhs.number_of_cars
    }
}

impl Add for &Garage {
    type Output = u64;
    fn add(self, rhs: Garage) j-> Self::Output {
        self.number_of_cars + rhs.number_of_cars
    }
}

impl Add<&Garage> for Garage {
    type Output = u64;
    fn add(self, rhs: &Garage) j-> Self::Output {
        self.number_of_cars + rhs.number_of_cars
    }
}

impl Add<&Garage> for &Garage {
    type Output = u64;
    fn add(self, rhs: &Garage) j-> Self::Output {
        self.number_of_cars + rhs.number_of_cars
    }
}

注意,在生成的代码中,有 4 个实现来表示在 Garages 中添加汽车数量时所有可能的使用情况,而在所有这些情况下,函数体都是相同的。这是由于Rust能够自动确定访问结构体成员所需传播的级别,与C++不同,在C++中我们需要具体指定,并使用解引用、点操作符和/或箭头操作符的组合,具体取决于输入是否是引用对象。

现在,我们可以以任何顺序使用运算符来处理 borrowed 和/或 owned 数据。

let garage_a = Garage { number_of_cars: 4 };
let garage_b = Garage { number_of_cars: 9 };

let total = garage_a + garage_b;
let total = &garage_a + garage_b;
let total = garage_a + &garage_b;
let total = &garage_a + &garage_b;

[注意!] 请记住Rust的隐藏 move 语义,并且如果尝试同时进行所有 total 赋值,代码将无法编译。非引用数据在调用时就会被移出作用域,并且将不再在原始创建的作用域中可用。

有关Rust数据所有权的官方信息可以在以下链接找到:这里这里

对于熟悉C++11及更高版本的读者,可以阅读以下链接了解更多信息:这里

用法

impl_op

在总结中,我们介绍了impl_ops,这是一个生成借用数据和所有数据代码的宏。impl_op(注意末尾缺少's)是一种以常规方式重载运算符而不为借用数据生成变体的一种方式。

#[opimps::impl_op(Add)]
fn add(self: Garage, rhs: Garage) -> u64 {
    self.number_of_cars + rhs.number_of_cars
}

这生成以下1:1的实现。

impl Add for Garage {
    type Output = u64;
    fn add(self, rhs: Garage) -> u64 {
        self.number_of_cars + rhs.number_of_cars
    }
}

这意味着我们只能这样做,不能做更多。

let garage_a = Garage { number_of_cars: 4 };
let garage_b = Garage { number_of_cars: 9 };

let total = garage_a + garage_b;

assert_eq!(13, total);

/* Neither of the three lines below will work! */
// let total = &garage_a + garage_b;
// let total = garage_a + &garage_b;
// let total = &garage_a + &garage_b;

与我们在总结示例中展示的impl_ops相比,这本身并不太有用,但它允许我们根据我们的设计选择对实现进行微调。

如果我们想要重载运算符,其中只有运算符的左侧是借用类型,那么我们可以按照以下方式实现。

#[opimps::impl_op(Add)]
fn add(self: &Garage, rhs: Garage) -> u64 {
    self.number_of_cars + rhs.number_of_cars
}

这生成了以下内容。

impl Add for &Garage {
    type Output = u64;
    fn add(self, rhs: Garage) -> u64 {
        self.number_of_cars + rhs.number_of_cars
    }
}

现在我们可以做&garage_a + garage_b

let garage_a = Garage { number_of_cars: 4 };
let garage_b = Garage { number_of_cars: 9 };

let total = &garage_a + garage_b;

assert_eq!(13, total);

/* Neither of the three lines below will work! */
// let total = garage_a + garage_b;
// let total = garage_a + &garage_b;
// let total = &garage_a + &garage_b;

同样,我们可以使用impl_op或其他不同类型来组合借用数据。

// borrowed right hand side
fn add(self: Garage, rhs: &Garage);

// borrowed both sides
fn add(self: &Garage, rhs: &Garage)

// Using a different type so that we can do something like `garage_a + 2`
fn add(self: Garage, rhs: u64)

impl_ops

impl_ops在底层使用impl_op来生成借用和所有数据组合的二进制运算符的实现。

use core::ops::Mul;

struct A;
struct B;
struct C;

#[opimps::impl_ops(Mul)]
fn mul(self: A, rhs: B) -> C { ... }

以上将生成以下内容。

impl Mul<B> for A { type Output = C; ... }
impl Mul<B> for &A { type Output = C; ... }
impl Mul<&B> for A { type Output = C; ... }
impl Mul<&B> for &A { type Output = C; ... }

impl_ops_lprim和impl_ops_rprim

有些情况下,我们想要为借用数据生成代码,但其中一个元素是原始类型。如果我们使用impl_ops,这可能会引起问题。因此,创建了impl_ops_lprimimpl_ops_rprim来解决这个问题;分别代表左侧原始类型和右侧原始类型。

impl_ops_lprim

#[opimps::impl_ops_lprim]
fn add(self: u64, rhs: Garage) -> u64 {
    ...
}

impl_ops_rprim

#[opimps::impl_ops_lprim]
fn add(self: Garage, rhs: u64) -> u64 {
    ...
}

impl_uni_op

impl_op实现二进制运算符,而impl_uni_op实现一元运算符。

struct Person {
    has_cars: bool
}

#[opimps::impl_uni_op(core::ops::Not)]
fn not(self: Person) -> Person {
    Person { has_cars: !self.has_cars }
}

impl_uni_ops

impl_ops生成二进制运算符的借用和所有数据实现类似,impl_uni_ops生成一元运算符的借用和所有数据实现。在底层,impl_uni_ops的实现使用impl_uni_op

给定以下struct

struct Person {
    has_cars: bool
}

可以为Person实现一元运算符!,如下所示

use core::ops::Not;

#[opimps::impl_uni_ops(Not)]
fn not(self: Person) -> Person {
    Person { has_cars: !self.has_cars }
}

现在我们应该能够做以下操作

let a = Person { has_cars: true };

let res = !(&a);
let res = !a;

impl_op_assign

我们可以实现基于赋值的运算符,如+=*=-=

pub struct TestObj {
    pub val: i32
}

#[opimps::impl_ops_assign(std::ops::AddAssign)]
fn add_assign(self: TestObj, rhs: TestObj) {
   self.val += rhs.val;
}

let mut a = TestObj { val: 4 };
let b = TestObj { val: 7 };

a += b;
assert_eq!(11, a.val);

let mut a = TestObj { val: 4 };
let b = TestObj { val: 7 };
a += &b;

assert_eq!(11, a.val);
assert_eq!(7, b.val);

泛型

我们可以像使用标准函数的泛型一样为impl_opsimpl_uni_ops使用泛型。

use std::ops::Add;

pub struct Num<T>(pub T);

/// ```
/// use opimps::impl_ops;
/// use mycrate::Num;
/// 
/// let a = Num(2.0);
/// let b = Num(3.0);
/// 
/// let res = a + b;
/// assert_eq!(5.0, res.0);
/// ```
#[opimps::impl_ops(Add)]
fn add<T>(self: Num<T>, rhs: Num<T>) -> Num<T> where T: Add<Output = T> + Copy {
    Num(self.0 + rhs.0)
}

实际示例

到目前为止,我们只展示了无用的示例,但这是因为这些示例被简化了,以便于理解。以下是一个示例,它使用了SIMD指令来计算x86_64架构的四元数乘法。虽然这不是完整的源代码,但这只是一个片段,展示了如何使用opimps实现数学库。

// No explanation of quaternions will be provided here since it involves a lot of theory. You only need to know that it's used to perform 3D rotations while avoiding the issues of gimbal locking that occurs from performing rotations using euler angles.

/// ```
/// use noname::v32::quat::Quat;
/// let l = Quat::<f32>::new(7.0, 1.0, 9.0, 4.0);
/// let mut r = Quat::<f32>::new(9.0, 4.0, 8.0, 2.0);
///
/// let res = &l * &r;
/// r.j = 5.0; r.k = 7.0;
/// 
/// let r = Quat::<f32>::new(9.0, 5.0, 7.0, 2.0);
/// 
/// let res = Quat::from(res);
///
/// assert_eq!(  22.0, res.i);
/// assert_eq!(  43.0, res.j);
/// assert_eq!(  69.0, res.k);
/// assert_eq!(-131.0, res.s);
/// 
/// let res = l * r;
/// let res = Quat::from(res);
/// 
/// assert_eq!(  12.0, res.i);
/// assert_eq!(  54.0, res.j);
/// assert_eq!(  72.0, res.k);
/// assert_eq!(-123.0, res.s);
/// ```
#[opimps::impl_ops(Mul)]
fn mul(self: Quat<i32>, rhs: Quat<i32>) -> Computable {
    let l = self.as_slice();
    let r = rhs.as_slice();

    let s = (&self.s * &rhs.s) - (&self).dot(rhs.clone());

    let v1 = Computable::from(l);
    let v2 = Computable::from(r);

    let s1 = Computable::all((&self).s);
    let s2 = Computable::all((&rhs).s);

    let s1v2 = s1 * v2;
    let s2v1 = s2 * v1;

    let v1xv2 = self.cross(rhs);
    
    let mut res = s1v2 + s2v1 + v1xv2;
    
    unsafe { crate::insert_i32!(res, s, 3) };
    return res;
}

依赖关系

~1.5MB
~35K SLoC