Live data from Hacker News

Almost Always Unsigned

graphitemaster.github.io

81–90 of 103 posts

Re: Almost Always Unsigned

#81

using int64 = int64_t; using nat64 = uint64_t; There are no unsigned integers. Naturals, folks, naturals have no sign. Or, if you must, the positive integers: using pos64 = uint64_t;

Naturals have no 64-bit limit.

Re: Almost Always Unsigned

#82
post #63

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'…

I believe the main issue lies in most programming languages lacking theorem proving capabilities to prove the safety of integer operations. The safety conditions for unsigned arithmetic: Ensure y+x ≤ INT_MAX. If x ≤ UINT_MAX-y, then x+y evaluates correctly: ∀x∀y(x ≤ UINT_MAX-y → ∃z(z = y+x)) Ensure y-x ≤ INT_MAX. If x≤y, then y-x evaluates correctly: ∀x∀y(x≤y → ∃z(z = y-x)) The safety conditions for signed arithmetic…

What do you mean by notation like:

    Ensure y+x ≤ INT_MAX.
Is this supposed to be a precondition? Why would I want this precondition when using unsigned arithmetic?

Re: Almost Always Unsigned

#83

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 thr…

Loop counters really are not the problem. The problem is that even when using unsigned integers you often want to do 'signed math' on them (e.g. adding a negative amount, or you could have an expression made entirely of unsigned integers (like ((x - y) + z) where an intermediate result may become negative even when the end result is positive - and in languages with overflow check that may result in a panic). A better…

But are we necessarily limited to native integer types? At least with C++, the type system is powerful enough to support integer replacement types (eg. [0][1]) that don't inherit these issues. (Where, for example, the subtraction of an unsigned from another unsigned returns a value of signed integer type.) Another advantage being the ability to customize the overflow/underflow handling policy per declaration (rather than relying on a compiler flag that applies globally).

[0] https://www.boost.org/doc/libs/develop/libs/safe_numerics/do...

[1] https://github.com/duneroadrunner/SaferCPlusPlus/blob/master...

Re: Almost Always Unsigned

#84
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?

I was actually thinking that's kind of genius, even if it is a bit too subtle. Probably better than casting though.

(Of course the best thing is real range types like in Rust.)

Re: Almost Always Unsigned

#86

There was a golang proposal to change Go's default int type to arbitrary precision big int. It would avoid overflow bugs, but the proposal was closed (after eight years) due to concerns about compatibility reading serialized data and performance. https://github.com/golang/go/issues/19623

IMO that's one of the few good decisions Python made. You simply don't have to think about overflow, underflow, signed or unsigned.

But I'm not sure it makes sense in a language like Go where they actually care about performance.

Re: Almost Always Unsigned

#87
post #20

Earlier quoted context omitted.

I liked how the discussion of 'delta = x - y' moved right on to how really you usually want delta = abs(x - y), so let's talk about that instead... Even beyond Stroustrup, Dijkstra, and Google, this whole panel of C++ luminaries agrees to prefer signed types and explains pretty clearly why: - 12:12-13:08 - https://www.youtube.com/watch?v=Puio5dly9N8#t=12m12s - 42:40-45:26 - https://www.youtube.com/watch?v=Puio5dly9N8…

Thanks for the excerpts! I was trying to understand the reasoning, which seem to just be in the 2nd excerpt: - The rules of signed/unsigned are complicated and there is too much auto-conversion - does that mean languages that make this more explicit means this is fine? It just seems ideal to have stronger typing. - It is mentioned that you can initialized an unsigned int to "-2" - but that presumably could also be fi…

> I'm trying to separate out which is "don't do this in C/C++" and which is "don't do this in any language".

To achieve high performance, any language would need to implement integer addition with a single machine instruction like 'ADD'. Languages can achieve more intuitive behavior by adding an operand check before the 'ADD', or by using an 'ADC' instruction and checking the carry bit afterwards. But adding branch statements to every add operation would slow computation significantly. Clever languages/compilers might deduce certain invariants and variable ranges that enable it to remove some of these branches to help in certain special cases.

Evidently Rust has an optional "safe add" that adds the branches to check for overflow. So newer languages offer more explicit options. But the core issue is more fundamental than language-specific.

Re: Almost Always Unsigned

#88

Earlier quoted context omitted.

Loop counters really are not the problem. The problem is that even when using unsigned integers you often want to do 'signed math' on them (e.g. adding a negative amount, or you could have an expression made entirely of unsigned integers (like ((x - y) + z) where an intermediate result may become negative even when the end result is positive - and in languages with overflow check that may result in a panic). A better…

But are we necessarily limited to native integer types? At least with C++, the type system is powerful enough to support integer replacement types (eg. [0][1]) that don't inherit these issues. (Where, for example, the subtraction of an unsigned from another unsigned returns a value of signed integer type.) Another advantage being the ability to customize the overflow/underflow handling policy per declaration (rather…

I would rather do away with signed vs unsigned integer types completely, and instead have sign-agnostic integers like down on the assembly level (the world has settled on two's-complement anyway). Signed-vs-unsigned only matters in one situation: when extending a narrow integer type to a wider integer type (e.g. "sign-extension"), and this could be an explicit operation.

Beyond that, the decision whether a number is signed or unsigned is only needed for string formatting (e.g. it's like Schroedingers integers: whether a number is signed or unsigned only matters when you actually look at the number).

Re: Almost Always Unsigned

#89
One interesting feature of signed vs unsigned in languages where signed overflow is undefined is that signed numbers _behave_ like mathematical integers, whereas unsigned integers (because overflow is valid behavior) _do not_ behave like mathematical integers. For example: if you write a loop that sums numbers from 1 to N, for signed integers the compiler can assume the expression won’t overflow and emit the standard mathematical closed form, because unsigned can overflow it has to account for this and use a different closed form (if one exists for the unsigned case). IMO unsigned should only be used when you want to manipulate the bits of an integer and not the integer itself, but C and C++ fight you when you try to do this because of how ingrained size_t is.

Re: Almost Always Unsigned

#90
post #60

Earlier quoted context omitted.

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 }

Put the "--" in the condition. It's less ugly.

Pedantically, that doesn't properly invert post-increment in the loop step. It decrements one extra time. If I need to use the loop index after the loop, then decrementing in the condition would cause problems.

  size_t i = size;
  while(i-- > 0){
    // loop body, possibly break
  }
  use(i); // i wraps below 0

  size_t i = size;
  while(i > 0){
    i--;
    // loop body, possibly break
  }
  use(i); // i doesn't wrap
Practically, i leaves the for-loop scope, so most never encounter this problem.
Post reply on HN