Home > Enterprise >  Rust method handling number overflows and signed/unsigned fields with generics
Rust method handling number overflows and signed/unsigned fields with generics

Time:12-16

I am currently dealing with a question on Rust, hope this is not a too stupid question. The problem I am trying to solve is the following:

I want to have a struct witch two numeric fields, x and y. These numeric fields can be either signed or unsigned. These fields should only be numeric, float or int, but can be signed or unsigned.

The struct has one method called offset, which allows to increase or decrease the values of the fields. However, the behaviour of the offset method has to change depending on if the fields of the struct are signed or unsigned, because if the fields are unsigned they can never be negative. This problem is illustrated below:

use std::ops::AddAssign;

#[derive(Debug)]
struct Coords<T: AddAssign> {
    x: T,
    y: T,
}

impl<T: AddAssign> Coords<T> {
    /// Allows to change the values of the fields
    fn offset(&mut self, dx: T, dy: T) {
        self.x  = dx;
        self.y  = dy;
    }
}

// This works well for signed number. The offset method works for signed integers.
let x: i8 = 0;
let y: i8 = 0;
let mut coords = Coords{ x, y };
coords.offset(-10, -10);
println!("{:?}", coords); // Coords { x: -10, y: -10 }

// However, this does not work if x and y were unsigned
let x: u8 = 0;
let y: u8 = 0;
let mut coords = Coords{ x, y };
// This does not compile.
// Should give the result as Coords { x: 0, y: 0 }
// coords.offset(-10, -10); 

I am trying to write an implementation of the offset method as to handle the scenarios where the fields of Coords can be either float or int, signed or unsigned. What would be the cleanest and most idiomatic way of solving this?

I tried playing around with traits from the num crate, but no success so far. I think I could just write two structs, one for unsigned fields and another for signed fields, each of this with a different implementation of the offset method, but I am pretty sure that there is a more idiomatic solution for this in Rust.

Thank you :)

CodePudding user response:

You could write a trait to represent your number and the allowed operations on it. Something like the following:

trait CoordType : Copy {
    type Signed;
    fn offset(self, d: Self::Signed) -> Self;
}

You could also add AddAssign and/or SubAssign as requirements for this trait, depending on the intended usage, but they are not needed for this simple example, because the offset() function takes care of everything.

The impls for i8 and u8 would be as follows:

impl CoordType for i8 {
    type Signed = i8;
    fn offset(self, d: Self::Signed) -> Self {
        self.saturating_add(d)
    }
}

impl CoordType for u8 {
    type Signed = i8;
    fn offset(self, d: Self::Signed) -> Self {
        if d >= 0 {
            self.saturating_add(d as u8)
        } else {
            self.saturating_sub(-d as u8)
        }
    }
}

I'm using saturating_add/sub instead of plain and - to avoid panics on overflows. If you need that for i16, u16, i32, u32, etc. then a macro would help a lot.

The implementations for f32 and f64 should be also quite straightforward.

And then your Coords type is trivially implemented as:

#[derive(Debug)]
struct Coords<T: CoordType> {
    x: T,
    y: T,
}

impl<T: CoordType> Coords<T> {
    fn offset(&mut self, dx: T::Signed, dy: T::Signed) {
        self.x = self.x.offset(dx);
        self.y = self.y.offset(dy);
    }
}

Here is a link to the playground with that code.

  • Related