3

The problem I recently meet requires to do integer operation with boundary based on the bits of integer type.

For example, using i32 integer to do add operation, here's a piece of pseudo code to present the idea:

sum = a + b
max(min(sum, 2147483647), -2147483648)

// if the sum is larger than 2147483647, then return 2147483647.
// if the sum is smaller than -2147483648, then return -2147483648.

To achieve this, I naively wrote following ugly code:

fn i32_add_handling_by_casting(a: i32, b: i32) -> i32 {
    let sum: i32;
    if (a as i64 + b as i64) > 2147483647 as i64 {
        sum = 2147483647;
    } else if (a as i64 + b as i64) < -2147483648 as i64 {
        sum = -2147483648;
    } else {
        sum = a + b;
    }
    sum
}

fn main() {
    println!("{:?}", i32_add_handling_by_casting(2147483647, 1));
    println!("{:?}", i32_add_handling_by_casting(-2147483648, -1));
}

The code works well; but my six sense told me that using type casting is problematic. Thus, I tried to use traditional panic (exception) handling to deal with this...but I stuck with below code (the panic result can't detect underflow or overflow):

use std::panic;

fn i32_add_handling_by_panic(a: i32, b: i32) -> i32 {
    let sum: i32;
    let result = panic::catch_unwind(|| {a + b}).ok();
    match result {
        Some(result) => { sum = result },
        None => { sum = ? }                                                              
    }
    sum
}

fn main() {
    println!("{:?}", i32_add_handling_by_panic(2147483647, 1));
    println!("{:?}", i32_add_handling_by_panic(-2147483648, -1));
} 

To sum up, I have 3 questions:

  1. Is my type casting solution valid for strong typing language? (If possible, I need the explanation why it's valid or not valid.)
  2. Is there other better way to deal with this problem?
  3. Could panic handle different exception separately?
pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
Kir Chou
  • 2,980
  • 1
  • 36
  • 48
  • 1
    Are you aware of [`saturating_add`](https://doc.rust-lang.org/std/primitive.i32.html#method.saturating_add)? – L. F. Sep 03 '20 at 08:38
  • 2
    Catching panic is probably a bad idea for performance reasons. Rust panics are not designed to be used as business exceptions and should be caught very rarely, if ever. Also, casting here is a mere conversion, which is both safe and well-defined, and there is nothing wrong with using it. It's the casts between pointers of different types that are dangerous, and those aren't even possible to dereference in safe Rust. – user4815162342 Sep 03 '20 at 08:43
  • @L.F. Thanks. I wasn't aware the series of saturating functions in primitive type. The one is exact what I need. I'll learn the implementation by tracing the source code later. – Kir Chou Sep 03 '20 at 08:49
  • 1
    Type casting with `as` can be problematic because it can change the value. The fix for that is to use a conversion that doesn't change the value: `From`. For example, write the first condition as ``if i64::from(a) + i64::from(b) > i64::from(i32::MAX)``. (That is, if it weren't obviously easier just to use saturating arithmetic.) – trent Sep 03 '20 at 15:00
  • @trentcl Thanks. `From` is another new knowledge for me. If you don't mind, could you provide an example that the value is changed after `as` type casting? – Kir Chou Sep 03 '20 at 16:11
  • 1
    Any time the value doesn't fit in the type being casted to. For example, `-10_i32 as u32` is `4294967286`, and `1_000_000_000 as i16` is `-13824`. If you happen to get the width or signedness wrong, you may just silently get the wrong value. `From` does not have this problem because integers only implement `From` when the conversion is lossless. (For conversions that are fallible, you may want to use `TryFrom`, which returns a `Result` that you can deal with in whatever way seems appropriate.) – trent Sep 03 '20 at 16:22

1 Answers1

12

In this case, the Rust standard library has a method called saturating_add, which supports your use case:

assert_eq!(10_i32.saturating_add(20), 30);
assert_eq!(i32::MIN.saturating_add(-1), i32::MIN);
assert_eq!(i32::MAX.saturating_add(1), i32::MAX);

Internally, it is implemented as a compiler intrinsic.


In general, such problems are not intended to be solved with panics and unwinding, which are intended for cleanup in exceptional cases only. A hand-written version might involve type casting, but calculating a as i64 + b as i64 only once. Alternatively, here's a version using checked_add, which returns None rather than panics in case of overflow:

fn saturating_add(a: i32, b: i32) -> i32 {
    if let Some(sum) = a.checked_add(b) {
        sum
    } else if a < 0 {
        i32::MIN
    } else {
        i32::MAX
    }
}
L. F.
  • 19,445
  • 8
  • 48
  • 82