Adding unsigned and signed integers

Today I had a really simple problem: add a signed and unsigned number of the same size. No solution I (and rkruppe on #rust) found were 100% satisfactory:

  • unsigned.wrapping_add(signed as usize): Doesn't panic when unsigned overflows like normal addition in debug. Eg 0usize.wrapping_add(-1isize as usize) won't panic.
  • (unsigned as isize + signed) as usize: Panics in debug if the signed addition overflows, even though that would not have overflown the unsigned integer. Also like the above, it doesn't panic if the unsigned value overflows negatively.
  • if signed < 0 { unsigned -= -signed as usize; } else { unsigned += signed as usize; }: Long code is long. Also, it cannot be used as an expression in the same way the prior two solutions can.

I can of course create a function that does all the necessary checks, but shouldn't something like this be part of the standard library (or even standard operators)?

20 comments · 94% upvoted · other discussions

comments

chubsauce
(if signed < 0 { unsigned - (-signed) as usize } else { unsigned + signed as usize })

is an expression. It can be made into a macro if you aren't fond of the size.

Noctune [OP]

-isize::min_value() will panic, unfortunately. So this isn't a solution and neither is the third one I wrote above.

heinrich5991

Try replacing (-signed) as u* with -(signed as u*), that will remove the overflow check.

Noctune [OP]

Seems to work, but Rustc warns that unsigned negation will be feature gated in the future.

heinrich5991

You can simulate unsigned negation by writing (!(signed as u*)).wrapping_add(1). LLVM will optimize this to unsigned negation.

EDIT: Checked that LLVM actually optimizes this (removed "likely") and changed wrapped_add to wrapping_add.

[deleted]

.wrapping_neg() seems to exist, too

Hauleth

But why it should panic in first case? If you cast -1 to unsigned type you will get usize::max_value(). If you want to make safe cast between two numeric types you should use num::cast which return None if there is no possible conversion.

But really I don't see the point. Probably you should write us what is your whole problem, not just one part of it.

Noctune [OP]

I want addition between unsigned and signed with the same overflow semantics as between unsigned and unsigned. Eg. 0usize - 1usize will panic, so 0usize + -1isize should panic as well.

Hauleth

But you are doing that (assume that usize == u8 for simplicity):

0usize + (-1isize as usize) == 0b00000000usize + (0b11111111isize as usize)

This is because x86 use U2. So what you get is:

0b00000000usize + 0b11111111usize == 0 + 255 == 255

This is how computer works and you should learn that in college/university/whatever. as is simple binary cast, it doesn't know anything about sign of number that it is casting.

EDIT:

In the case with subtraction:

0usize - 1usize = 0b00000000 - 0b00000001usize

And that is impossible as will overflow. If we use 0usize.wrapping_sub(1usize) then the result will be the same as above.

shepmaster

This is how computer works and you should learn that in college/university/whatever.

Remember that not every programmer follows this particular path, and not every programmer actually needs to know this type of information. Let people learn, don't chide them for asking ^_^.

Noctune [OP]

I'm not quite sure what you mean.

I care only about whether the result overflows, not whether the intermediate computation overflows. Eg in 1usize + -1isize the intermediate computation overflows, but the resulting value has not overflown: it is still mathematically correct. But in 0usize + -1isize the intermediate computation and the result has overflown, so we should panic in debug.

Hauleth

You are still thinking in mathematical way. In CS we operate on bits, so 0usize - 1usize is completely different operation than 0usize + -1isize.

I will be more explicit:

In U2 (two's complementary) binary representation of signed integers we compute negative numbers as:

-a == !a + 1 // where ! is binary negation

So -1i8 is !0b00000001 + 1 == 0b11111110 + 1 == 0b11111111. As 0b111111111 is also perfectly valid unsigned number (255) it can be binary casted, but it will completely different number. On the other hand it cannot be mathematically casted as -1 has no representation in unsigned world. Rust as operator is binary cast, so it returns perfectly valid number 255. No overflows, no panics.

About your example 1usize + (-1isize as usize) it will overflow as 1 + 255 == 256 which exceeds range of u8.

So you need to understand that binary cast isn't mathematical cast. And usize - usize isn't the same as usize + -isize.

Noctune [OP]

No, I understand two's complement just fine. I'm in CS myself. I don't understand how your explanation provides a solution to my problem.

I'm asking if there is an easy way to do addition between unsigned and signed integers that carry the same overflow semantics as the usual Rust addition operators (ie. they panic if they can't represent the value). Let's say we have such a function f. We could call f(0, -1) and it would panic, due to the mathematically correct result -1 not being representable as unsigned. Likewise, f(1, -1) would not panic regardless of the intermediate computations, as 0 is representable in unsigned.

Hauleth

It doesn't. I just asked why you think that first expression that you give should work.

To be honest you have your solution above - just use if. If you need to use this a lot then wrap it in macro. But IMHO it is still issue with design, not with cast.

[deleted]

[deleted]

Noctune [OP]

Wrapping is necessary because negative values result in values that will wrap when added due to two's complement. For example -1isize as usize == usize::max_value(). So 1usize.wrapping_add(usize::max_value()) == 0usize. It's still different from the unsigned value overflowing, like if you add 0usize and -1isize.

krdln

I think this should work (it's not pretty, but it's without if):

((unsigned as isize).wrapping_add(isize::min_value()) + signed)
    .wrapping_sub(isize::min_value()) as usize

So it's basically shifting "left" the domain of usize to domain of isize before adding, and then back "right" after. It feels like these two conversions are reusable and could be made into separate functions.

From your response to /u/chubsause it seems like he suggested something similar and then edited?

chubsauce

This doesn't fit with the semantics they wanted, because (unsigned as isize) will panic if unsigned is bigger than isize::max_value(). They want unsigned to be able to be any usize value, and signed to be any isize value, and for the result to only panic if it wraps around the domain of usize.

krdln

I'm pretty sure that as conversion on integers was decided to never panic (I don't remember where it's documented though). I recall I've tested this code on usize::max_value() and it worked on debug mode.

heinrich5991

Sorry, couldn't resist: You probably shouldn't be using isize (and usize in this case), use i32 or i64 depending on what you need.

Noctune [OP]

In my case, the unsigned value is an array index, so usize is best in this case.