Operator Overloading

I am trying to implement -= operator between two u8 buffers, unfinished test code here:

   use std::ops::SubAssign;
   type Binbuf =  Vec<u8>;
   impl SubAssign for Binbuf {
    fn sub_assign(&mut self, other: Self) { 
    self[self.len()-1] -= other[other.len()-1]
    }
}

but it complains that I use existing types, does not even like the new one defined especially instead of Vec.
Apparently it expects all types to be entirely defined from scratch in the current crate and have no relationship whatsoever to any existing types? Any hints please how to resolve this simply, if it is at all possible?

The error message says:
impl doesn't use only types from inside the current crate

type does not create a new type, but rather just a new name for the old type. You'll have to make a wrapper for that, like so (untested):

use std::ops::SubAssign;

struct Binbuf(Vec<u8>);
   
impl SubAssign for Binbuf {
    fn sub_assign(&mut self, other: Self) { 
      self.0[self.0.len()-1] -= other.0[other.0.len()-1]
    }
}
3 Likes

If you still want to access all the methods of the wrapped type, you should consider implementing Deref and DerefMut for the wrapper type.

But it's explicitly stated as not recommended. See the document.

I'm new to all this so you got me curious about overloading.

I don't know how you want to subtract arrays but after some tinkering this works:

use std::ops::SubAssign;

struct Binbuf(Vec<u8>);

impl SubAssign for Binbuf {
    fn sub_assign(&mut self, other: Self) {
        assert_eq!(self.0.len(), other.0.len());

        for i in 0..self.0.len() {
            self.0[i] -= other.0[i]
        }
    }
}

fn main() {
    let mut a: Binbuf = Binbuf {0: vec![10, 20, 30]};
    let b: Binbuf = Binbuf {0: vec![1, 2, 3]};

    println!("a = {:?}", a.0);
    println!("b = {:?}", b.0);

    a -= b;

    println!("a = {:?}", a.0);
//    println!("b = {:?}", b.0);     // move error
}
$ cargo run
...
a = [10, 20, 30]
b = [1, 2, 3]
a = [9, 18, 27]

Sadly that second printing of 'b' fails because Binbuf does not implement copy.

It's not clear to me why sub_assign needs to take ownership of it's RHS. Surely it should never mutate that?

1 Like

It really just needed a simple modification, saving self.0.len() in an intermediate variable. I don't see any errors about the types... Feels very simple to me, except the implementation probably isn't fully what you want in the end.

1 Like

You may be interested in looking up some material about the orphan rule, which is why you get the error regarding using types not in your crate.

Basically the purpose of the rule is to make it impossible for different crates to introduce conflicting impl blocks.

3 Likes

Good point. I guess, in this case, you simply want to make wrapped type publically accessible, so you can simply call wrapper.0.push(...) or whatever you want. After all, it was an alias, previously, so why bother making it private, now.

Thank you all for your help. I have now knocked it into shape, after a lot of trouble because I forgot to use iter_mut() instead of iter(). Anyway, assuming for now that a>=b, the following code does the right thing:

use std::{ops::SubAssign};

pub struct Binbuf(pub Vec<u8>);
const UMAX:u8 = std::u8::MAX;

impl SubAssign for Binbuf {
    fn sub_assign(&mut self, other: Self) {
		let length = self.0.len();
		if length != other.0.len() {
			mypanic(file!(),line!(),column!(),"Vecs subtracted must be of the same length!") };
		let mut carry = false;
		self.0.iter_mut().enumerate().rev().for_each( |(i,x)| {
			let y = other.0[i];
			// println!("i:{},x:{},y:{}",i,x,y);
			let (dif,car) = x.overflowing_sub(y);
			// println!("i:{},dif:{},car:{}",i,dif,car);
			if carry { 
				if dif == 0 { *x = UMAX } 
				else { *x = dif-1 } }
			else { *x = dif };
                        carry = car

	   });
	() }
}
#[test]
fn subop() {
   let mut a: Binbuf = Binbuf {0: vec![19,20,30]};
   let b: Binbuf = Binbuf {0: vec![18,255,35]};
   println!("a = {:?}", a.0);
   println!("b = {:?}", b.0);
   a -= b;
   println!("a = {:?}", a.0);
   assert_eq!(a.0,[0,20,251]);
}

running 1 test
a = [19, 20, 30]
b = [18, 255, 35]
a = [0, 20, 251]
test tests::subop ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

It is essentially bignum subtraction of &[u8] buffers, read from binary files, as the basic type. It would probably run quicker to convert to &[u64] but then one needs to worry about the leftover bytes and it hardly seemed worth the bother. Next I am doing the binary division.

You now have the same problem I has above. This fails with a move error. The operator eats the right hand operand:

   let mut a: Binbuf = Binbuf {0: vec![19,20,30]};
   let b: Binbuf = Binbuf {0: vec![1,1,1]};
   println!("a = {:?}", a.0);
   println!("b = {:?}", b.0);
   a -= b;
   a -= b;                       // error:  use of moved value: `b`
   println!("a = {:?}", a.0);
1 Like

Well, that is Rust for you. I guess I could implement Copy trait for it and that might "solve it" but unnecessary copying does not square up with the "zero cost" objective.

Maybe someone else has a solution?

No, you can't implement copy on it as it contains a Vec. You want

impl<'a> SubAssign<&'a BinBuf> for Binbuf

This allows you to take the second argument by reference.

7 Likes

alice

Ha, that works:

use std::{ops::SubAssign};

struct Binbuf(Vec<u8>);

impl<'a> SubAssign<&'a Binbuf> for Binbuf {
    fn sub_assign(&mut self, rhs: &Self) {
        assert_eq!(self.0.len(), rhs.0.len());

        for i in 0..self.0.len() {
            self.0[i] -= rhs.0[i]
        }
    }
}

fn main() {
    let mut a: Binbuf = Binbuf {0: vec![10, 20, 30]};
    let b: Binbuf = Binbuf {0: vec![1, 2, 3]};

    println!("a = {:?}", a.0);
    println!("b = {:?}", b.0);

    a -= &b;
    a -= &b;
    println!("a = {:?}", a.0);
    println!("b = {:?}", b.0);

}
a = [10, 20, 30]
b = [1, 2, 3]
a = [8, 16, 24]
b = [1, 2, 3]

Yes, I was thinking on the same lines, even trying it but did not get those lifetime parameters right.
As always, Alice in Rust WonderLand comes through with the magic incantation :sparkling_heart:

In the meantime, I have also written somewhat simpler solution that gives up on the ops:

pub trait Vecarith {
	fn subass( &mut self, other: &[u8]) -> ();
}

impl Vecarith for Vec<u8> {

	fn subass(&mut self, other: &[u8]) {
		let length = self.len();
		if length != other.len() {
			mypanic(file!(),line!(),column!(),"Vecs subtracted must be of the same length!") };
		let mut carry = false;
		self.iter_mut().enumerate().rev().for_each( |(i,x)| {
			let y = other[i];
			let (dif,car) = x.overflowing_sub(y);
			if carry { 
				if dif == 0 { *x = UMAX } else { *x = dif-1; carry=car } }
			else { *x = dif; carry=car }
	   }); () 
	}
}

#[test]
fn submeth() {
   let mut a = vec![19,20,30];
   let b =  vec![18,255,35];
   println!("a = {:?}", a);
   println!("b = {:?}", b);
   a.subass(&b);
   println!("a = {:?}", a);
   println!("b = {:?}", b);
   assert_eq!(a,[0,20,251]);
}
2 Likes

Aside:

Do you know the dbg! macro? It's pretty handy for this kind of things :slightly_smiling_face:

dbg!(i, x, y);
1 Like

Yes but I found out pretty soon that lots of things don't have Debug implemented and so I find it easier and more consistent to always write my own. Still have not quite fathomed the "convenience" of having to fuss around with derive debug pragmas and what not.... Plus I like to debug sometimes in release mode instead of filling up huge disk space with superfluous debug directories.

Finding the bugs is not the problem. The problem is guessing the right rusty incantations to make them go away, see above.

Yes, how does alice's solution above actually work? I have been investigating for some time now and still have no idea.

If you look at the documentation for the SubAssign trait:

pub trait SubAssign<Rhs = Self> {
    fn sub_assign(&mut self, rhs: Rhs);
}

you can see that the first (and only) type parameter Rhs ("right hand side") defaults to the same type as Self. Since Self=Binbuf, the sub_assign method will take its rhs parameter by value. Since Binbuf is not a copy type, it will be moved into the sub_assign function and subsequently dropped when returning.

Instead, if you implement SubAssign for Binbuf where the right-hand-side is a &Binbuf, it'll just borrow the argument instead of consuming it. FWIW, it works fine without the explicit lifetime annotations for me -- it's possible that they're not strictly required for this simple example. My lifetime-fu is not sufficiently strong to answer that part.

In other words, you can instead just use:

impl SubAssign<&Binbuf> for Binbuf { ... }

Thank you for that.

As a little niggle I have to say that of course the first thing I did was find the SubAssign in std::ops - Rust documentation page.

When one arrives there one does not see what you have shown. At your prompting I find that if I hit the little [+] button by "Show declaration" I get to see what you are talking about.

Now, if I understand what it is saying, by way of default type etc, is another matter. As a newbie it's not so clear.

But really, if a document is to document a "thing", surely the "thing" should be prominent. Not hidden behind a button as if it was some kind of source code one does not need to know about.

There is no regular language description of what SubAssign is at all.

I do get the RTFM thing. But the FM should be clear before offering that advice.

1 Like