14

Do I have to branch on the sign of the signed integer, like in the example below? (In the real program y is calculated to change the index pointing into a 64K array, and wraparound is the desired behavior)

fn main() {
    let mut x: u16 = 100;
    let y: i8 = -1;
    //x += y;
    if y < 0 {
        x -= i8::abs(y) as u16;
    } else {
        x += i8::abs(y) as u16;
    }
    println!("{}", x);
}
Alexey
  • 1,354
  • 13
  • 30
  • Possible duplicate of [How to multiply/divide/add/subtract numbers of different types?](https://stackoverflow.com/questions/44552219/how-to-multiply-divide-add-subtract-numbers-of-different-types) – E_net4 Nov 23 '18 at 23:15
  • 1
    @E_net4iskindandwelcoming the linked question is more about floats vs integers, not mixed signedness – Alexey Nov 23 '18 at 23:19
  • 2
    The intuition, as well as the course of action, are the same. Arithmetic between integers of mixed signedness is tricky, and could have odd results if the compiler just chose an unexpected conversion ruleset (this is the case for C and C++). Just use the `from` associated function if the conversion is lossless (e.g. `i16::from(y)`) and the `as` operator otherwise. – E_net4 Nov 23 '18 at 23:25
  • 2
    `x = x.wrapping_add(y as u16);`? – Veedrac Nov 23 '18 at 23:39
  • Or better yet, `x.wrapping_add(i16::from(y))`, to conform with the [`cast_lossless`](https://rust-lang.github.io/rust-clippy/master/index.html#cast_lossless) Clippy lint. – E_net4 Nov 23 '18 at 23:44
  • See also [this Reddit thread on the topic](https://www.reddit.com/r/rust/comments/3mcwf7/adding_unsigned_and_signed_integers/), which discusses how to add an `isize` and a `usize`. The only way to get this right in all cases appears to be `(if signed < 0 { unsigned - (-signed) as usize } else { unsigned + signed as usize })`. – Sven Marnach Nov 24 '18 at 14:20
  • @E_net4iskindandwelcoming, `x.wrapping_add(i16::from(y))` results in `expected u16, found i16 [E0308]`, as it's still adding a signed and an unsigned int together – Alexey Dec 02 '18 at 14:05
  • Yes, you'd need to convert `x` as well. My comments were more focused on the existence of multiple numeric conversion methods. – E_net4 Dec 02 '18 at 14:39

3 Answers3

10

Rustc 1.66+ support mixed integer ops. Note, this was previously unstable under mixed_integer_ops.

With this you can do:

#[feature(mixed_integer_ops)]
fn main() {
    let mut x: u16 = 100;
    let y: i8 = -1;
    //x += y;
    x = x.wrapping_add_signed(y); // wraps on overflow
    println!("{}", x);
}

There's wrapping_, checked_, saturating_, and overflowing_ variants, depending on what behavior you want on overflow; and the methods are on both signed and unsigned integers, named _unsigned and _signed respectively (so the above could also be expressed as x = y.wrapping_add_unsigned(x);)

Juan Campa
  • 1,181
  • 2
  • 14
  • 20
Zoey Hewll
  • 4,788
  • 2
  • 20
  • 33
7
  • Signed integers are twos complement
  • numeric cast sign extends an i8 as u16

That means you can cast y as u16 it will turn into a twos complement value that a wrapping_add will rightfully handle if y was negative.

In short: do as @Veedrac said.

fn main() {
    let mut x: u16 = 100;
    let y: i8 = -1;
    x = x.wrapping_add(y as u16);
    println!("{}", x);
}
kmkaplan
  • 18,655
  • 4
  • 51
  • 65
  • 1
    I would not recommend doing this, but instead using the next, higher, signed value instead (in this case `i32`) and add them. – hellow Nov 24 '18 at 07:18
  • Thank you, I forgot that that Rust panics on unsigned overflow, not just on signed. – Alexey Nov 24 '18 at 17:14
  • 2
    @hellow care to elaborate? First this seem much more complicated and error prone, and secondly at some point (`u128`) you just can't. – kmkaplan Nov 25 '18 at 08:07
1

In the real program y is calculated to change the index pointing into a 64K array , and wraparound is the desired behavior)

There is no consensus of how things should be done, but here's my advice: Provide to the user two functions:

fn add(&mut self, index: u16) -> u16 { // return previous index why not
    // ..
}

fn sub(&mut self, index: u16) -> u16 {
    // ..
}

You could also add a helper function that should not be used lightly:

fn offset(&mut self, offset: i16) -> u16 {
    // ..
}

The purpose is that the user should know whenever use sub or add, the user should manage only unsigned type. That question is opinion oriented so I understand if people don't agree.

Full example:

use std::mem;

#[derive(Debug, PartialEq, PartialOrd)]
struct MyIndex {
    index: u16,
}

impl MyIndex {
    fn new(index: u16) -> Self {
        Self { index }
    }

    fn add(&mut self, index: u16) -> u16 {
        let index = self.index.wrapping_add(index);
        self.replace(index)
    }

    fn sub(&mut self, index: u16) -> u16 {
        let index = self.index.wrapping_sub(index);
        self.replace(index)
    }

    fn offset(&mut self, offset: i16) -> u16 {
        if offset > 0 {
            self.add(offset as u16)
        } else {
            self.sub(offset as u16)
        }
    }

    fn replace(&mut self, index: u16) -> u16 {
        mem::replace(&mut self.index, index)
    }
}

fn main() {
    let mut index = MyIndex::new(42);

    let mut other_index = MyIndex::new(84);

    let (x, first) = if index > other_index {
        (index.index - other_index.index, true)
    }
    else {
        (other_index.index - index.index, false)
    };

    // ...

    if first {
        index.sub(x);
    }
    else {
        other_index.sub(x);
    }
    println!("{:?} {:?}", index, other_index);

    index.sub(21);
    println!("{:?}", index);

    index.offset(-1);
    println!("{:?}", index);
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Stargateur
  • 24,473
  • 8
  • 65
  • 91