Live data from Hacker News

Almost Always Unsigned

graphitemaster.github.io

51–60 of 103 posts

Re: Almost Always Unsigned

#51
post #41

Earlier quoted context omitted.

> Makes for horrible footguns like: > history[counter % SIZE] = … The footgun here is that the “modulo” operator does not actually calculate the modulo in C. In Python, this works correctly for negative values.

> In Python, this works correctly for negative values. They’re both “broken” in different ways. Arguably C’s brokenness is more apparent and less useful but Python also has footguns: C uses truncated division for its “modulo” so the remainder has the sign of the dividend, Python uses floored division so the remainder has the sign of the divisor instead. The wiki page for modulo has a pretty extensive page on the subj…

> Python uses floored division so the remainder has the sign of the divisor instead

Serious question, how is that a footgun? In decades of software development I have never needed a negative divisor for modulo. What would you use it for?

Re: Almost Always Unsigned

#52
post #41

Earlier quoted context omitted.

> Makes for horrible footguns like: > history[counter % SIZE] = … The footgun here is that the “modulo” operator does not actually calculate the modulo in C. In Python, this works correctly for negative values.

> In Python, this works correctly for negative values. They’re both “broken” in different ways. Arguably C’s brokenness is more apparent and less useful but Python also has footguns: C uses truncated division for its “modulo” so the remainder has the sign of the dividend, Python uses floored division so the remainder has the sign of the divisor instead. The wiki page for modulo has a pretty extensive page on the subj…

I say you want a pair of operations such that (a, b) = quotient_and_remainder(x, y) gives you a and b such that a * y + b = x

The Euclidean division and remainder work, the other division and remainder also work, and they're both identical for the positive integers so people who only think about the positive integers won't even notice there's a choice here. So I like that Rust provides both pairs, in the same way Rust provides both Wrapping and Saturating because maybe you mean wrapping overflow or maybe you mean saturating overflow and we should make you choose not just assume we know best.

Re: Almost Always Unsigned

#53
The entire reason for integer under/overflow to be undefined is to enable compiler optimisations. If you're going to be using unsigned anyway, we might as well drop the undefined behaviour from the standard and just say it's machine-defined. That should honestly be the correct choice. If a loop is hot enough to benefit from those optimisations, you can easily rewrite it in a form that makes the compiler assume overflow won't happen. Either using current syntax with unreachable(), or we can add a runtime_assume(expr) expression that signals to the compiler that it can assume expr is true. Though for full safety I would prefer using if(likely(expr)) {fast code} else {panic or return error}.

As an aside, unsigned does not save you from undefined behaviour. When sizeof(short)==2 and sizeof(int)==4 (e.g. x86, x64, arm32, arm64), then multiplying two unsigned short values happens by upcasting them to ints (see integer promotion rules), which can overflow the int.

My personal opinion is that along with making signed overflow defined, unsigned integers should be entirely removed as a type and there should instead be separate signed vs unsigned operators, because at the processor level there is no difference between the two, and there hasn't been a good case to separate them at the hardware level for the last ~half century. Basically, do what Java does with some syntax like unsigned{expr} which forces all integers inside expression to be treated as unsigned. Unsigned literals can stay, but they will be bitcast to signed equivalents if used outside unsigned context.

Re: Almost Always Unsigned

#54
post #50

The article over-downplays the need for sentinel values. Surely Rust has Option that also spreads into C++ these days as std::optional. But for plain C using -1 or negative values to denote sentinels or error code is rather nice idiom. The argument will be more valid if array indexes will be 1-based like in Fortran/Matlab/Julia as then 0 becomes extremely nice sentinel values. But C is C and needs -1.

Not disagreeing about the need for sentinel values, but Rust also has NonZero. When combined with Option, you get a similar result to outcome to C's -1.

Similar because it's a compiler hack, as in the compiler treats std::num::NonZero specially. You can't create your own type with the same properties as you can in C.

Re: Almost Always Unsigned

#56
I have a better solution: address the root cause of unsafe semantics by not using raw indexed for-loops, unless one absolutely needs an index, in which case one should generate it with std::views::enumerate. To reverse it, use std::views::enumerate | std::views::reverse. Ditto for languages with similar semantics.

I almost never write a raw for-i loop any more, especially since 99% of the time I want to enumerate through the entire array or vector, and I can just use a ranged for-loop to do that. It allows me to redesign my code around the data, express things at a higher level, and my code looks far more SIMDable and reminiscent of array programming languages. And yet it is safer, I will never see under/overflow or any of these old-hat problems.

If you are using C, then too bad, you're stuck with a language that doesn't allow the programmer to more meaningfully and more clearly express intent at a higher level of abstraction without paying additional runtime costs.

This stuff compiles to broadly the same assembly.

Re: Almost Always Unsigned

#57
In a language that has arbitrary precision integers, you'd pretty much never want them unsigned, or even to have signed and unsigned flavors.

Whether unsigned or signed is better is a matter that is a combination of personal opinion and the quirks of a given systems programming fixed integer language.

The trade-off reasoning would be different, for instance, in a language that requires implementations to provide two's complement signed integers, with wraparound semantics. Or, say, no wraparound semantics but a robust overflow detection system coupled to exception handling.

There are other matters beside overflow, like conversions. In C, mixtures of signed and unsigned bring in some implementation-defined conversion rules, which nudges the argument toward "all unsigned" or "all signed" for the sake of avoiding mixtures.

I like to trot out the following argument.

Suppose a, b and c are small integers close enough to zero that any additive/subtractive combination of them is free of overflow.

If they are signed, then we can make inequality derivations like

  a + b 
If they are unsigned, then we cannot do this. That is a barrier to refactoring code with arithmetic conditionals and just reasoning about it.

Re: Almost Always Unsigned

#60
post #3

> for (size_t i = size - 1; i Erm... just because you can, doesn't mean you should. Also, what if you want to go down to something other than 0?

Post-increment inverts to pre-decrement, but for-loops don't support proper syntax sugar for pre-decrement.

  for(size_t i = 0; i  0;){ i--;
    // loop body
  }
Post reply on HN