Adding signed and unsigned integers is verbose and error prone


#1

Basically, it is really difficult to add an unsigned and signed number in Rust without also opting out of overflow checking in the process.

I made a post on /r/rust about this a while ago, but recently stumbled into this problem again.

A few examples of how you might try to do it:

  • unsigned.wrapping_add(signed as usize) doesn’t check for overflow.
  • (unsigned as isize + signed) as usize will panic if the signed addition overflows, even though the resulting unsigned integer would not have overflown.
  • if signed < 0 { unsigned - -signed as usize } else { unsigned + signed as usize } panics for std::isize::MIN.
  • if signed < 0 { unsigned - signed.wrapping_neg() as usize } else { unsigned + signed as usize } works… I think. But is obviously very verbose and not very obvious.

Adding signed and unsigned integers seems like very basic functionality, so shouldn’t something like this be in the standard library? Or is there a better way I haven’t noticed?


#2

I’ve just hit this and found it surprisingly hard.

I’m trying to add an i8 to a u16. It would be nice if this was straight forward.


#3

It is straightforward; you only need to do the intermediary arithmetic in a representation that includes all possible values of each input type. In other words, to add an i8 and a u16, which have ranges [-128 . . +127] and [0 . . 65535], you need to do the computation in a type whose representation includes all values between -128 and 65535, inclusive. The minimum such representation in Rust is i32.

Of course overflow can still occur in such a larger representation, depending on the details of the computation, but at least arithmetic in the larger representation is defined for all potential sets of inputs.

The underlying problems discussed in this thread are due to conflating different mathematical rings of integers:
a) arithmetic in the infinite ring of all integers, which is learned at a young age, with
b) arithmetic in the finite ring of unsigned integers implied by a given storage representation, and
c) arithmetic in the finite ring of signed integers implied by the same storage representation.

Arithmetic in the latter two mathematical rings “wrap” between the minimum value of the representation and the maximum value of the representation. Conventional add, subtract and multiply operators on integers are defined only on a); they do not “wrap”, and thus the result of an operation in ring a) potentially can overflow the representation of the input values.

The add, subtract and multiply operators of the Rust Wrapping type, and Rust’s wrapping_add, wrapping_sub and wrapping_mul operators, are defined on b), and separately on c), but not on a mixture of b) and c). These operators never overflow; instead they “wrap”, providing the remainder of the operation (op) modulo the representation size. For these operators, when x and y are both in uN the result in uN is

     (x op y) % (1 << N)

and when x and y are both in iN the result in iN is

     (x op y + (1 << N - 1)) % (1 << N) - (1 << N - 1)

where the additional complexity over simple

     (x op y) % (1 << N - 1)

exists to handle a result of -(1 << N - 1) correctly. (To see why the simpler formula amost works, recall that the result of the % operator has the sign of the first operand.)

Note that overflow can also occur whenever a value of one type is converted to a different type whose range does not include the first type. Thus the result of the i32 computation of adding an i8 and a u16 can overflow when converted to either u16 or i16, truncating high-order bits of the result value with respect to the output representation.


#4

< deleted , i was going to post something that I posted elsewhere on a similar theme but i see the focus here is slightly different r.e. my own issues>


#5

Why is your post empty?