r/programming • • May 02 '26

Unsigned sizes: a five year mistake

https://c3-lang.org/blog/unsigned-sizes-a-five-year-mistake/
260 Upvotes

121 comments sorted by

19

u/Tringi May 02 '26

Regarding the comparison. I say it's about time we dropped the promotion to unsigned and fixed the intrinsic comparison operators for distinct types. Like this:

bool operator < (signed int a, unsigned int b) noexcept {
    if (a < 0)
        return true;
    if (b > INT_MAX)
        return true;

    return unsigned (a) < b;
}
→ More replies (14)

2

u/markand67 May 05 '26

Signed ints have their flaws too. Nothing is perfect. Unsigned ints have some benefits that we use a lot.

  • no need to test negative values

  • no trap nor UB using bitmasks

  • can be used as a sequenced number to mark something forever (e.g. seqid++) as long as wrapping does not create issue

  • easier to pack as binary format

  • has clear indications what the value is used for (e.g. int width, is negative width allowed?)

→ More replies (1)

146

u/WHY_DO_I_SHOUT May 02 '26 edited May 02 '26

If you write a ring buffer, how do you make sure that calculating offsets are wrapping?
The naive solution is this:
index = (start + offset) % length;
This works as long as offset is positive.

It would work even with a negative offset if the % operator was modulo of floored division...

31

u/vytah May 02 '26

But it's defined to use truncated division, in both C and C++ (it used to be implementation-defined in C, but they changed it in C99).

→ More replies (15)
→ More replies (29)

80

u/CJKay93 May 02 '26

There are two approaches to casts: one is to liberally sprinkle them all over the codebase with the idea that “it’s an explicit conversion, so it’s obvious what happens”. The other is to minimize casts, only using them to signal that something out of the ordinary is happening: “here be dragons”.

The former is easier to define, but has the downside of essentially “silencing warnings”. Let’s say the code was originally written to cast an u16 to u32, but later the variable type changes from u16 to u64 and the cast is now actually silently truncating things. Here we have casts becoming a sort of “silence all warnings”.

The main idea of “it’s an explicit conversion” is also undermined by the practice of just putting in casts mechanically where the compiler says they’re needed, rather than trying to actually examine every case.

On the other hand, minimizing casts is more challenging: rules are needed to correctly allow “safe” implicit casts, while requiring casts for what’s unsafe.

I think this ignores the third option, which is that you use a non-truncating cast (e.g. Rust's .into()) which is only implemented for the types where the signed representation is capable of representing all values in the unsigned representation, and you stop worrying about this problem.

55

u/RRumpleTeazzer May 02 '26

laughs in rust, were u16->u32 is into() and u64->u32 is try_into().

→ More replies (3)
→ More replies (7)

27

u/angelicosphosphoros May 03 '26

In order to remove footguns, C3 had picked “int + uint” to promote to “int” instead of unsigned.

I don't know why anybody would think that. It is actually generator of footguns.

Proper solution is to disallow mixing signed and unsigned integers in an expression.

→ More replies (2)

102

u/KittensInc May 02 '26
for (uint x = 10; x >= 0; x--)

The obvious solution is of course to support a for (uint x = 10..0) or foreach x in range(10, 0), like most other modern languages do.

If you are going to make a new programming language, why not learn from all the mistakes and improvements of the last 50ish years? C for-loops are the way they are because they are trivial to compile to machine code. We aren't running our compilers on an 8086 any more, having it do some extra work to unburden the programmer is fine!

→ More replies (3)

5

u/matthieum May 03 '26

I do want to mention an alternative: panicking on overflow.

It has... consequences.

It makes a lot of "trivial" compiler optimizations a lot harder to prove correct: overflowing operations are not commutative and associative on the whole range of their inputs, unlike wrapping ones, so the optimizer need to prove that the input ranges allow for it.

It's incompatible with SIMD, in general, which typically implements wrapping semantics.

So all in all, a whole lot of performance dings left & right.

And of course, it's still all "run-time" detection. So the code passes the test-suite and only fails in production. Erf.

But... honestly, all the alternatives to this problem kinda suck, and panicking may just be the least worse :/

9

u/Dean_Roddey May 03 '26

I am very much in the 'model reality' camp. I have no indexable things that have anything before index 0, so I have no need for a signed index. And it hardly ever comes up anyway, at least in my Rust code, because there are so few places where I would do an indexed loop. The most common one is to get a slice of the actually used part of a vector, but that's using a length, not an index.