I am writing a type which should support an arithmetic operator via both Op
and OpAssign
. Let's take +
and +=
as a concrete example.
My natural instincts make me want to ensure that all the following cases work:
a + b;
&a + b;
a + &b;
&a + &b;
a += b;
a += &b
Moving the LHS of +=
makes no sense, so there are only 2 (rather than 4) cases
for +=
and the borrow is implicit on the LHS.
It's pretty clear to me how to implement support for any single one of these
cases in isolation, but I have 2 major problems in the big picture:
-
The borrow and move overloads seem to interfere with each other in ways that
I don't fully understand. -
I find myself unable to reuse the logic in some of the cases.
For the sake of illustration, let's take the concrete example of a 2-dimensional
vector. In C++ the whole lot can be implemented with something along the lines of
class Vec2D {
public:
Foo(float x, float y): x(x), y(y) {}
Foo& operator+=(const Foo& other) {
this->x += other.x;
this->y += other.y;
return *this;
}
friend Vec2D operator+(Vec2D lhs, const Foo& rhs) {
lhs += rhs; // reuse operator+= defined above
return lhs;
}
private:
float x;
float y;
};
Points to note:
- All 6 variants listed at the top of the question are covered by this implementation.
- Only 2 functions need be defined to cover all the cases.
- The core logic is implemented in only one of these functions and is reused in the other.
The Rust equivalent:
use std::ops;
#[derive(Debug, Clone, PartialEq)]
pub struct Vec2D {
x: f32,
y: f32,
}
impl Vec2D {
fn new(x: f32, y: f32) -> Self { Vec2D{x, y} }
}
impl ops::Add for Vec2D {
type Output = Self;
fn add(self, other: Self) -> Self {
let mut res = self.clone();
res += other;
res
}
}
impl ops::AddAssign for Vec2D {
fn add_assign(&mut self, other: Self) {
self.x += other.x;
self.y += other.y;
}
}
gets me as far as passing these two tests
#[test]
fn add_move_both() {
let a = Vec2D::new(2.0, 3.0);
let b = Vec2D::new(4.0, 5.0);
assert_eq!(a + b, Vec2D::new(6.0, 8.0));
}
#[test]
fn add_assign_move_rhs() {
let mut a = Vec2D::new(2.0, 3.0);
let b = Vec2D::new(4.0, 5.0);
a += b;
assert_eq!(a, Vec2D::new(6.0, 8.0));
}
which cover only 2 of the 6 cases listed at the top of this question.
Trying to cater for the four remaining cases (those involving borrowing the
operands) leads to the conflicts and code duplication in mentioned above.
How should these operators be implemented in Rust?