thread 'main' panicked at 'attempt to subtract with overflow'
Even more so now that custom profiles have been added to Cargo (yay), you can enable overflow checking in release, and add a `release-unchecked` or whatever.
3u64.checked_sub(4u64).expect("Overflow");
This allows you to spell out that you don't expect this to overflow, and Rust should panic if it does, regardless of your compiler flags.
Much simpler to just toggle on the relevant flag.
But surely that's almost never the case? Few programs actually desire or care for base 2 modular arithmetics, and when they do it tends to be for very specific tasks (usually cryptographic or cryptography-adjacent e.g. hashing, checksumming, ...).
Which is why it’s not so impractical to make an occasional exception.
However, changing - to perform checked_sub() leaves you with an Option, and this makes ordinary arithmetic look pretty clumsy:
let x = (a-b).unwrap()-c;
The intent is to land more types with intrinsic behaviour. Today only the Wrapping type is provided, so Wrapping<i32> is an i32 that definitely has Wrapping arithmetic and won't panic on overflow, but eventually Saturating<i16> will be possible (e.g. for CD audio PCM samples, saturating arithmetic is correct, if you try to make the loudest possible noise louder it just stays the same) and so will Unchecked<i8> if you really sure that you're doing 8-bit arithmetic that can't overflow and doesn't need Debug checks.
If it was an int at the function signature and you got -1 what would you expect to happen? It's the same thing.
How could you possibly check this using types that cannot represent what you are checking for?
`f(u32 a, u32 b) { assert a > b ... `
(if the hypothetical following operation is `a-b` as discussed higher in the thread)
Log the error and act accordingly, instead of believing this kind of issue only happens during development
If input data is faulty and the reason for that is not seen as part of your logic, then sure, log an error and skip.
I used to define my own assert macro to "ensure" my asserts aren't disabled, but I don't bother anymore. There's nothing wrong with assert, and you needn't define NDEBUG. It's important to be aware that there are different situations, and not all warrant aborting, as described above. Another differentation is that there can be asserts that must be disabled in release builds for performance reasons, and others that won't affect performance and can stay enabled.
The same thing, but worse, can happen with signed integers. (-3)-INT_MAX is either some huge positive number, or something crazy because it's undefined behavior and the compiler is allowed to do anything it wants.
I can understand where they come from, especially regarding error handling: for integers whose value must not be negative, it is easier to check for underflow by checking if the result is not negative rather than by checking that the result is not eg smaller than the previous value (eg for addition).
That being said, "number must not be negative" really ought to be encoded in the type system, so that the user of the type knows that they must actually look for underflow.
I guess that part of the problem is that we don't want to check for underflow after each operation, so checking the negativity is a way to "coalesce" several checks after multiple operations. However this is fragile, because the multiple operations could end up producing a positive value, even if some intermediate values where negative.
For full safety, I don't see how we could do better than checking after each operation right now. If we're doing this, I feel like checked_add and friends from rust is a better fit than cramming an unsigned int into a signed one.
I wonder if we could design an integer type with 63 bits of value, plus one bit of "overflow/underflow poison", such that any operation that would under/overflow would saturate that bit to one, but otherwise still perform the operation on the value part. That would allow to coalesce multiple checks while keeping safety even in the presence of multiple faulty operations. I wonder how it could be implemented efficiently though
There is something like that already for floating point. Whenever an overflow or underflow happens, it sets a sticky bit in a separate flags register. You can clear these flags, do a sequence of operations, and at the end, see if any of these flags are set. See https://man7.org/linux/man-pages/man3/fenv.3.html for the standard C API for it.
[0] Supplemental Integer Safety, http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2868.pdf
In terms of the C standard, "3-4" for unsigned ints is modular arithmetic, and the "wrong thing" is assuming that it will do anything other than wrap around. This is very clearly defined, and implied whenever you see an arithmetic expression on unsigned integers.
C made the right tradeoff IMO. You can protect yourself against overflow if you need to, but if it always signals errors you can’t turn that off.
Maybe you want to do some weird bit banging trickery, and that's fine too, but it should require an out-of-the-way function call.
Many languages optimize numeric operations and DX around numeric types for efficiency rather than correctness, and make the latter high-friction. (Wrap-around subtraction, decimal literals being treated as IEEE floats, etc.)
The exceptions (or cases where it is merely less true) tend to be very high level, dynamic languages.