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

opimps

一个简单的属性宏库,用于帮助对借用和所有数据重载运算符

6 个版本

0.2.2 2023年5月30日
0.2.1 2023年5月30日
0.1.4 2021年2月18日
0.1.1 2020年12月8日

过程宏 中排名第 137

Download history 53/week @ 2024-03-11 61/week @ 2024-03-18 58/week @ 2024-03-25 68/week @ 2024-04-01 59/week @ 2024-04-08 77/week @ 2024-04-15 45/week @ 2024-04-22 34/week @ 2024-04-29 30/week @ 2024-05-06 92/week @ 2024-05-13 86/week @ 2024-05-20 68/week @ 2024-05-27 46/week @ 2024-06-03 77/week @ 2024-06-10 84/week @ 2024-06-17 101/week @ 2024-06-24

每月下载量 314
11 Crate 中使用 (直接使用 6 个)

Apache-2.0

31KB
346

opimps

opimps 简化了Rust的运算符重载,使其可以像C++一样编写,但无需重复代码。

摘要

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

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

想象我们有一个存储汽车的车库。

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++则需要我们具体指定,并使用解引用操作符、点操作符和/或箭头操作符的组合,具体取决于输入是否为引用对象。

我们现在可以使用运算符来处理 借用 和/或 所有 数据,顺序不限。

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;
}

依赖关系

~280–730KB
~17K SLoC