Almost Always Unsigned

graphitemaster.github.io

51 points by gavide a day ago


delta_p_delta_x - 2 hours ago

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.

Dylan16807 - 16 minutes ago

> 0x7ffffffffffffffff. The typical argument is that such a value would be “pathological”. Not only is this argument incorrect, it’s even more dangerous which we will see later.

The only later thing I see that's somewhat relevant to that appears to be dealing with 32 bit overflow? That doesn't prove the argument incorrect.

No current OS I'm aware of lets you have a size_t that goes over 2^63. I doubt any OS will ever allow it. It makes things easier if virtual memory is capped to 2^62 or so, and if anyone really ends up with a use case for more I expect them to switch to 128 bit numbers.

shaggie76 - 42 minutes ago

While I'm a fan of unsigned (size_t mostly) there have been a few times when the tax for converting them to float was shockingly high:

https://godbolt.org/z/96T4jTshc

1-2 instructions for signed vs 11 including a branch for unsigned.

(in times like these I found casting to signed first preferable)

fpoling - 3 hours ago

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.

kazinator - 2 hours ago

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 < c

      b < c - a    // subtract a from both sides; "bring to other side"
If they are unsigned, then we cannot do this. That is a barrier to refactoring code with arithmetic conditionals and just reasoning about it.
dataflown - a day ago

Stroustrup recommends int over unsigned. Dijkstra recommends int over unsigned. Google coding guidelines recommend int over unsigned.

Blogger recommends unsigned over int.

Tough choice.

jgtrosh - 20 hours ago

> Where unsigned does benefit here is when these are used as indices into an array. The signed behavior will almost certainly produce invalid indices which leads to memory unsafety issues. The unsigned way will never do that, it’ll stay bounded, even if the index it produces is actually wrong. This is a much less-severe logic bug, but can still be used maliciously depending on context.

The argument for signed often goes that it's easier to detect an invalid operation on an index by checking for bounds, or the higher probability of segmentation faults, than if your index is always “valid”. Granted, even with signed your invalid operation might still give a valid index. Simply put, seeing a negative index in your debugger is an obvious red flag you lose with unsigned.

In general I want to complain about the idea that a subtle bug is less severe than an obvious bug. Obvious bugs can be caught automatically or manually and are therefore less severe than subtle ones. This is a mistakes all students do btw. Segmentation faults are your friends!

lerno - an hour ago

Counterpoint: https://news.ycombinator.com/item?id=47989154

pikuseru - a day ago

What if I’m making a 2d game and need to go left

karussell - 15 hours ago

As a Java developer I'm a bit sad that we don't have a choice :)

bogdanoff_2 - a day ago

You can count down to zero with while(i--)

scared_together - a day ago

There was a related article from the other side of the debate a while back: https://news.ycombinator.com/item?id=47989154

It’s pretty sad that after all these years, dealing with fixed size integers is still so complicated. Yes, many of the problems are specific to low level languages with undefined behaviour and numeric for loops. But the issue of subtracting two numbers and possibly having an underflow is both common and a bit absurd.

The code in the article for “safely” calculating the difference of two unsigned numbers, which is simpler than the equivalent for signed integers, is this little ritual:

> delta = max(x, y) - min(x, y);

Seriously??? Two function calls just for the difference of two numbers??

Why can’t such “safe” operations have some of the sweet syntactic sugar, and the underflow-rampant “ordinary” operations have the bitter medicine of ritual?

dataflow - a day ago

> for (size_t i = size - 1; i < size; i--)

Erm... just because you can, doesn't mean you should.

Also, what if you want to go down to something other than 0?

cpeterso - a day ago

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

Asooka - 2 hours ago

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.

pmarreck - a day ago

Related: I have a variable integer length encoding scheme that beats out LEB128, Protobuf, varint and ASN.1 while also encoding/declaring endianness (but notably, leaving signage information up to the application): https://github.com/pmarreck/BLIP

tialaramex - a day ago

Should be (2022) apparently - surely if HN can automatically screw up titles for various reasons, we can have it add dates automatically [and sometimes get those wrong] too? DanG ?

> for instance C and C++ leave signed integer wrap undefined

I'm pretty sure the way to write what was meant here is "C and C++ leave signed integer overflow undefined". Wrapping would be a definite choice. Overflow is the situation we're considering, not a particular outcome to choose so that is what's undefined.

The confusion gets worse later when it insists that just like in C or C++ these three Rust expressions will produce invalid results because of LLVM:

    x / 0
    INT_MIN / -1
    INT_MAX % -1
    INT_MAX - INT_MIN
Assuming we defined INT_MAX and INT_MIN as say i32::MAX and i32::MIN (or whichever signed type you prefer) of course what these actually do in Rust is just panic. If you write this in a context where it'll be evaluated at compile time, your compilation fails. That's not "invalid" in any sense I understand.

It mentions Odin too, I know less about Odin but I believe it too will reject this nonsense, on Godbolt it seems to either SIGILL (for zero) or SIGFPE (for other impossible operations)

Edited: Apparently I copy-pasted wrong? Some of those invalid expressions were not as written on the blog post but are now hopefully fixed. They are, of course, still not invalid in Rust, some panic because they aren't valid questions (like dividing by zero), others are fine - neither case is a problem.

theokrueger - a day ago

[dead]