Almost Always Unsigned (2022)
19 points by intelfx
19 points by intelfx
Previously discussed on Lobsters a few years ago.
Re-submitting prompted by by a complementary article posted recently.
Unfortunately, for languages based on LLVM, no amount of hand-waving and wanting signed wrapping to be defined will work no matter how hard you try, so such statements about safety are factually incorrect. Here’s a somewhat non-exhaustive list of all the undefined behavior of signed integer arithmetic in LLVM which applies to all languages which use LLVM:
This is completely wrong. A compiler can compile its code in a way that prevents the LLVM-level UB, and that's exactly what Rust does. In fact it's even more wrong for the subtraction example which is by default not UB in LLVM -- sub has wrapping semantics. One has to add an attribute to get "overflow is UB".
Most of the motivation of this article applies to C and C++, but examples for other languages such as Go, Rust and Odin will also be presented in an attempt to establish that this concept applies to all languages, regardless of their choices (for instance C and C++ leave signed integer wrap undefined), but rather is intrinsic to the arithmetic itself.
Unfortunately, the article does not live up to the expectation of providing language-independent arguments. The C/C++-specific UB on signed integer overflow is mentioned 8 times. The "Checking for overflow and underflow is easier and safer" section is entirely language-specific; just because C/C++ have no good APIs to check for signed overflow doesn't mean such an API is impossible (and I would argue x + y < x is still a bad way to check for overflow as it is far from self-explaining). It's also outdated, C23 added some APIs for this purpose (ckd_add etc).
If we disregard that part, the only arguments that seem to remain are
just because C/C++ have no good APIs to check for signed overflow doesn't mean such an API is impossible
C now has the horribly-named <stdckdint.h>, eg
int sum;
if(ckd_add(&sum, x, y)) {
// error
} else {
// use sum
}
This is a good explanation. I’ve used unsigned integers for indexes for decades until a few years ago when I got tired of casting pointer_end - pointer_start to size_t, have read many arguments for and against.
Signed indexes have worked fine for me but after reading this article, even if what it shows there is not new, has me now thinking about it again because it might make sense for me to use a function for the pointer difference (which I already do for strings) which would abort with an error message if pointer_end < pointer_start in which case the cast to size_t would stay out of the way inside that function.
I would miss the down-to operator though: for (int i = 12; i --> 0;) { ... }
It would become the less attractive for (unsigned i = 12; i --< 12;) {...} or for (unsigned i = 12; --i != SIZE_MAX;) {...}
Anything that requires the user to learn patterns different from basic maths is obviously wrong and doomed. Use unsigned when you need modular arithmetic, e.g. Z/pZ and use signed whenever you need normal arithmetic, unless you're happy with 0 - 1 == 4 billion, a much, much, much more common issue than anything that happens with signed integers. Or, rect2.x - rect1.x: kaboom with unsigned.
Z/pZ
Z/nZ is probably better notation here. Writing p often implies that the order is prime, which 2^k usually isn't.