Almost Always Unsigned
graphitemaster.github.io
graphitemaster.github.io
I'm now back in the signed camp. Signed integers make it a lot easier to sanity check integer subtraction when negative values are disallowed (a common situation), and it's easy to reason about. Also, not converting between signed and unsigned avoids accidental conversion overflows due to different expectations, so keeping it signed bypasses that whole can of worms while maintaining maximum flexibility.
Unsigned gives you 2x the positive numeric range, which 99.999% of the time you'll never use (and if you did, underflow detection would become a nightmare).
I can't think of a single time I've heard someone complain "I wish Java's long type supported up to 16 quintillion instead of a paltry 8 quintillion"
At the end of the day, code is for people, so keep it easy for people.
This adds more 'value' to the code for people than allowing them to switch their brains off because something is 'unlikely' to happen. A large class of bugs exists because people assume stuff is unlikely.
I also think one has to distinguish between API and actual code.
If you write a lib that takes inputs that should never be negative you can express this without writing a single line of documentation about it (or if you do, requiring the user to read it) by using unsigned types in these places.
You can still use signed types in your actual library code if you want/must/believe in that but do not pollute the outside API with them if unnecessary.
- dangers of mixed signed/unsigned code (signed values MUST exist, but unsigned values don't have to)
- more cognitive load to read and understand unsigned underflow detection code + issues once you exceed the halfway point
This is why Java offered signed integer types only (I think they should have allowed unsigned as well, but for different reasons)
Please show me an example in the wild that could not be trivially caught/alleviated. With 'trivially' I mean the code that needs to be written to prevent it, not the thinking you need to do to consider it.
As for the latter: this is what I get paid for. Writing the code out is necessary but not what I really get paid for (unless I were a very slow typist, that is).
I find that most of the time I was simply too lazy. Or rather: I wrote C/C++ code for 20+ years. I used int and didn't think (unintended rhyme).
Now I write (mostly) Rust I find myself considering the edge cases regularly and also covering them because the compiler forces me to. See also this comment [1].
I.e. a - 1 + b where a is always positive (incl. 0) and b is always > 1 'just' works with int (and unsigned [overflow] too!) in C/C++.
In Rust you will get a panic when a = 0 which will then make you think what you're doing and simply reorder this to a + b - 1.
I find that this is the 'cognitive load' that needs to be applied commonly and I'm pretty fine with that.
> dangers of mixed signed/unsigned code
Alleviated by simply not mixing. There is a reason languages like Rust simply do not allow this without explicit typecasts. And those, in cases where one type is signed and the other is not and they have equal width translate to "all bets are off".
> more cognitive load to read and understand unsigned underflow detection code [...]
See first point. IMHO this is simply the same point expressed differently. Underflow happens when you subtract.
Ensure you do not subtract a larger value from a smaller. This is as easy as writing max(a, b) - min(a, b) or what author of [1] does.
Same as abs(a - b) but works with unsigned. Cognitive load? I do not see any and I agree with the author of the article that this is easier to read.
People WILL forget to check, or implement the check wrong (and it won't be discovered until the next black swan event), or some invariant will change in future and nobody will know to update this code. The code doesn't care because the compiler doesn't enforce the invariants, so you're left with bare and likely confusing (and probably eventually undefined) behavior.
I for one would LOVE it if popular languages could be instructed to inject invariant enforcement code (not asserts) at certain boundaries. It would help with a whole host of problems!
When you cross zero with a signed integer calculation, it's easy to find out, and the code has low cognitive overhead. Also, with code generally structured this way, bad input propagation tends to get stopped earlier because of the number of functions that check against negative values.
It's also possible to underflow check with unsigned, but the code is more complicated and can't stop propagation if it fails, and if you are allowing values > the signed positive limit (or unwittingly allowing it), your overflow detection code gets even more complicated (or wrong) due to the reduced overhead space. You COULD do a bunch of checks beforehand, but that's not always feasible due to the combination of operations being done on the operands, and once again people are people.
Even though many modern languages block uncast type conversions, that still doesn't protect you from converting a negative integer to an unsigned integer. Yes, you SHOULD be checking before the cast, buuuuuuut...
What we want to do is reduce the number of ways things can go wrong, reduce the fallout of an errant process, and minimize cognitive load to reduce bugs overall. Making that worse just to double the positive integer space doesn't make sense to me.
unsigned int length_a = 10;
unsigned int length_b = 20;
if (length_a - length_b < 0)
{
printf("negative diff\n");
}
else
{
printf("positive diff\n");
}
I would argue that there is plenty of code like that in the wild. It looks innocent and harmless but it gives a completely surprising result.thread 'main' panicked at 'attempt to subtract with overflow'
Even more so now that custom profiles have been added to Cargo (yay), you can enable overflow checking in release, and add a `release-unchecked` or whatever.
3u64.checked_sub(4u64).expect("Overflow");
This allows you to spell out that you don't expect this to overflow, and Rust should panic if it does, regardless of your compiler flags.
Much simpler to just toggle on the relevant flag.
But surely that's almost never the case? Few programs actually desire or care for base 2 modular arithmetics, and when they do it tends to be for very specific tasks (usually cryptographic or cryptography-adjacent e.g. hashing, checksumming, ...).
Which is why it’s not so impractical to make an occasional exception.
However, changing - to perform checked_sub() leaves you with an Option, and this makes ordinary arithmetic look pretty clumsy:
let x = (a-b).unwrap()-c;
The intent is to land more types with intrinsic behaviour. Today only the Wrapping type is provided, so Wrapping<i32> is an i32 that definitely has Wrapping arithmetic and won't panic on overflow, but eventually Saturating<i16> will be possible (e.g. for CD audio PCM samples, saturating arithmetic is correct, if you try to make the loudest possible noise louder it just stays the same) and so will Unchecked<i8> if you really sure that you're doing 8-bit arithmetic that can't overflow and doesn't need Debug checks.
Maybe you want to do some weird bit banging trickery, and that's fine too, but it should require an out-of-the-way function call.
Many languages optimize numeric operations and DX around numeric types for efficiency rather than correctness, and make the latter high-friction. (Wrap-around subtraction, decimal literals being treated as IEEE floats, etc.)
The exceptions (or cases where it is merely less true) tend to be very high level, dynamic languages.
If it was an int at the function signature and you got -1 what would you expect to happen? It's the same thing.
How could you possibly check this using types that cannot represent what you are checking for?
`f(u32 a, u32 b) { assert a > b ... `
(if the hypothetical following operation is `a-b` as discussed higher in the thread)
Log the error and act accordingly, instead of believing this kind of issue only happens during development
If input data is faulty and the reason for that is not seen as part of your logic, then sure, log an error and skip.
I used to define my own assert macro to "ensure" my asserts aren't disabled, but I don't bother anymore. There's nothing wrong with assert, and you needn't define NDEBUG. It's important to be aware that there are different situations, and not all warrant aborting, as described above. Another differentation is that there can be asserts that must be disabled in release builds for performance reasons, and others that won't affect performance and can stay enabled.
In terms of the C standard, "3-4" for unsigned ints is modular arithmetic, and the "wrong thing" is assuming that it will do anything other than wrap around. This is very clearly defined, and implied whenever you see an arithmetic expression on unsigned integers.
C made the right tradeoff IMO. You can protect yourself against overflow if you need to, but if it always signals errors you can’t turn that off.
The same thing, but worse, can happen with signed integers. (-3)-INT_MAX is either some huge positive number, or something crazy because it's undefined behavior and the compiler is allowed to do anything it wants.
I can understand where they come from, especially regarding error handling: for integers whose value must not be negative, it is easier to check for underflow by checking if the result is not negative rather than by checking that the result is not eg smaller than the previous value (eg for addition).
That being said, "number must not be negative" really ought to be encoded in the type system, so that the user of the type knows that they must actually look for underflow.
I guess that part of the problem is that we don't want to check for underflow after each operation, so checking the negativity is a way to "coalesce" several checks after multiple operations. However this is fragile, because the multiple operations could end up producing a positive value, even if some intermediate values where negative.
For full safety, I don't see how we could do better than checking after each operation right now. If we're doing this, I feel like checked_add and friends from rust is a better fit than cramming an unsigned int into a signed one.
I wonder if we could design an integer type with 63 bits of value, plus one bit of "overflow/underflow poison", such that any operation that would under/overflow would saturate that bit to one, but otherwise still perform the operation on the value part. That would allow to coalesce multiple checks while keeping safety even in the presence of multiple faulty operations. I wonder how it could be implemented efficiently though
There is something like that already for floating point. Whenever an overflow or underflow happens, it sets a sticky bit in a separate flags register. You can clear these flags, do a sequence of operations, and at the end, see if any of these flags are set. See https://man7.org/linux/man-pages/man3/fenv.3.html for the standard C API for it.
[0] Supplemental Integer Safety, http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2868.pdf
So, let's make an odd_int_less_than_20587 type and a billion of other types? "Not negative" normally helps only one tiny bit towards documenting anything about the accepted values. Making a separate type for that and complicating everything is ridiculous.
> If you write a lib that takes inputs that should never be negative
Have you ever compared the difference between two unsigned integers? Because unsigned integers don't have "natural" subtraction. Did you ever find the need for signed numbers to interact with unsigned numbers? Because if you did, it's a bad idea to separate them without need. Premature isolation is a prime cause of complexity.
Bounded integer types have been extremely useful for me on a few occassions, and I wish more languages had type systems powerful enough to support them. They also address your concerns about subtraction.
They are not. I write mostly Rust these days.
Yes, what's wrong with that ? Every variable's type should be defined as precisely as possible when meaningful, that's the whole point (and C/C++ not easily allowing to do that is one of their biggest drawbacks imho.)
In fact most software is designed such that the physical sizes are chosen first, and then the practical bounds follow from that.
> void foo(ranged_int<1, 5> a, ranged_int<3, 7> b)
Why would I _ever_ write a function like that? Practical proofs need to be about runtime values, not just constants and types. They also need to put multiple values in relation. They need to handle type casts and many more things. Constant integer ranges might be of some use but it's a tiny fraction of the invariants that a programmer juggles in practice, and the source code is already blown up out of proportion just for these ranges. This thing is just not gonna fly.
Just multiply some "good" values a few times and there is no way that you can statically prove that the result is going to be in the range as well, even though it might always be the case due to how the values combine each time (simple example: offset + size that the code checked to be within array bounds). Invariants in practice are relations over the runtime values of multiple variables. The focus on single values is basically already where types fail as a correctness tool.
I don't understand, there's literally a CVE about that elsewhere in this thread, basically
struct my_structure {
int size;
int offset;
};
either you restrict both size and offset to e.g. 2^30 so that you can safely sum both, or you will have an issue because you'll be doing if(size + offset < max_size) and size + offset overflows because absolutely no one can be trusted to write overflow checks, you gotta use a library for that.Usually it's very easy - require that size and offset are validly indexing an object, and require that objects are of a sane size (I rarely have in-memory objects larger than a few megabytes, and for most code I posit that it is a mistake to not chunk and stream huge datasets). Done, no real need to worry.
If you cannot make any assumptions about the values contained in a "my_structure" then that means the code is at an API boundary, which is the perfect place where such check (if-statement) must be placed. At internal code places, checks are wasted effort, and at most a compiler switch to detect wraps (logic errors) would often be a much better choice than paranoid code where each line of code can't trust the previous line, and where one can't see actual work being done because it's all covered in mistrust. Unnecessary mistrust (i.e. mistrust at places strictly inside maintenance boundaries which are not strategic checkpoints) only makes it hard to write code that actually does something, and might possibly cause more bugs than it could ever save.
Yees?
Bounded integers are a relatively common feature of programming languages (and one which is sorely missing from Rust, it's technically possible currently but not super fun, when fleshed out enough const generics should make them much nicer, possibly even built-in)
If you genuinely wanted to reason solidly about such limits, you do end up needing arbitrarily bounded integers. Of course, most people don't want to do that, for good reasons. And even if you wanted it, there's a reasonable question whether adding them to the language syntax is really the right thing to do.
You could also argue that subtraction for unsigned numbers is only partially defined, just like for signed numbers. But that would be missing the point, since most numbers in practical programs are small, and for these numbers the signed range is indeed much more useful since subtraction is defined for signed integers (e.g. i32) for small numbers. This is not the case for unsigned, where 3u-4u is usually not what you want.
Perhaps the biggest issue all together is just that many languages will let you do 3u - 4u instead of outright making the syntax more obviously annoying to force you to think about the caveats and pick between {truncated, throwing, overflow} variants.
Yes, and at least in C++ it isn't that difficult to trivially generate these types as you need them with a modicum of template-fu. You don't need that many integer type templates to capture most of the common cases and provide contextually sane operators. This might be a little more difficult in other languages.
This is pretty standard type safety practice and usually well worth it in terms of writing robust low-defect code.
I do database engines. This has been a standard part of code standards for a really long time, at least as long as the programming languages we used practically supported it. I've done it ever since. The only type that is allowed to flow through the code is size_t, where it makes sense.
Are there any open source examples to study this style? I don't know where to look, but admittedly studying C++ codebases is not what I do for fun.
In my personal experience, it never worked putting layers of abstractions to hide physical representations. That makes the implementation code really hard to grok, and your hands are bound in the code because you're not allowed to _assume_. What worked for me instead is making sure that certain decisions that are likely to change are contained inside a module and for the rest of the code hidden behind an API or simply not accessible. Runtime assertions can be useful too, but maybe that is part of what you are describing by "template-foo". Finally, I try to stay true to my decisions and am not trying to act like I could have to replace floats by doubles at every corner. Some code that I've seen was littered with templates for that single reason, and I'm pretty sure the developers didn't have an intimate understanding how floating points works.
Interestingly, size_t is one of the things that I tend to avoid, and in my perception a majority of the programmer population agrees it was a mistake to have it be an unsigned type. A type which can represent "the size of an object" is too general for all but maybe OS APIs IMO. Usually I know that the size of my objects is bounded by a certain size, well below what fits in 32 bits.
This is one of the best features of the pascal family:
http://www.delphibasics.co.uk/Article.asp?Name=Sets
(look in "SubRanges")
That, plus the type incompatibility that arises from mixing such types with other integer types.
The ones that used it like it...
I see your point, but I think that (while not always), it can still be pretty helpful to add your custom types to make invalid states unrepresentable.
This can sound ridiculous on something like C, but if your language has refined types, it's not that bad.
For example, this doesn't look awful to me:
``` type WeirdId = Int Refined (Odd And Less[20587])
val myId: WeirdId = 5 // OK val myInt: Int = myId // Can be used as an Int val myRuntimeId: Either[String, WeirdId] = refineV[Odd And Less[20587]](myId + 2) // Runtime Check val myInvalidId: WeirdId = 6 // Compilation Error ```
My gut feeling is that the right way forward is to have a fairly standard type system visible in the source language augmented by contracts written in regular code. Basically a slight augmentation of assert(). A formal prover could internally reason about an extended type system where those contracts are automatically lifted to the type level if that helps for some reason, but programmers wouldn't be bothered with it.
This seems unlikely to be a genuinely novel idea, so I'd be curious to hear whether systems like that already exist.
There was a dialect of C#, Sing# or Spec#, that allowed you to specifying pre and post conditions in the same expression language as the main language, and they were checked at compile time. This is what comes closest I think.
In reality, like all C keywords, it's completely misleading and actually really means "weird_arithmetic_mode_that_will_mess_you_up".
Not sure what they mean about it being misleading though. They might prefer it if they were called "modular integers" instead of "unsigned integers" but that would lead to confusion with modules IMO.
Modular integers to me would seem very confusing. They're integers without the sign, and they have some max value determined by the number of bits, I can't really think of a better name for them except for the fixed-width versions which include that bit information.
Extremely intuitive, simple English words, exactly what it says it is (integer without the sign bit).
In short, the operations on `unsigned int` do not really behave like "non-negative integer"; as such it's a weird match to use them for that purpose.
Whether the operation then errors out or wraps doesn't change anything about unsigned being the right name for it, because it's something you wouldn't expect unsigned to do.
The intersection of your two NonEmptySet produces an empty set. If you write that result into a NonEmptySet it's the same, it doesn't make sense for you to expect a NonEmptySet as a result of that operation.
Imagine you have a bug in your program. If you intend for a value to be non-negative and use a signed integer, you get a negative value. Your code is broken. You didn't want a negative value. But you do have have a very easy way of checking for this at runtime, at least.
Now imagine you instead use an unsigned integer. The bug is still there but now it manifests as overflow or underflow. Your program is still broken. Nothing about using an unsigned integer saved you. And worse, you don't have an easy predicate to check that something has gone wrong.
I think this can only be a reasonable choice if you are working in a language that has dynamic checks for overflow and so using an unsigned integer enforces not only the predicate that the value is nonnegative but also that all operations stay within its expected arithmetic domain.
This is a poor, poor substitute for contract checking.
You see, doesn't make a lot of sense to distinguish between positive and negative in the end. From a mathematical point of view negative integers have the same dignity of positive integers since a couple of centuries. The same operations that apply on positive integers apply on negative integers, and not only that, an operation between two positive integers can as well result in a negative integer!
The unsigned type was created just to have one more bit if you *are sure* you have only positive integers. That could had a sense in the era of 8/16 computers. Nowadays that we have 32 or 64 bit computers does really that extra bit count that much? I don't think so... 2 billions is plenty enough for most usages.
Ada's "Positive" or "Natural" or
subtype X is Integer range Y..Z;
expresses intent (well expresses requirement really), it also enforces that constraint, so it's not merely an assumption.Yes, and let's start with overflow behavior because it's much more relevant than the type domain.
If overflow isn't encoded by the type, there is just no way to correctly set a strict domain.
My preference is to minimize surprise. If everyone is used to using signed types for everything, you better have a good reason for using something else, and leave that justification in a comment
Several APIs in the VFX arena (Pixar OpenSubD, Pixar USD, and Foundry's Katana GeoLib API) all use signed ints for indices which I've occasionally argued against, with the response being "Google's coding standard says don't use them".
As for 64-bit values, unless you can foresee requiring > 8 quintillion, unsigned isn't buying you anything.
There are a number of ways to handle this:
- Use unsigned 32-bit (gives you a little bit more runway, but you'll still hit the end hard)
- Use 64-bit values (ends your problem once and for all, but costs double the memory so now you have a new problem)
- Use 40-bit values (Gives you a lot more runway, but costs CPU)
I looked at OpenSubdiv extensively. There are no good reasons to use signed in the cases I opened the ticket for and making sure the code inside the lib does the right thing with unsigned arithmetic is trivial.
But someone still needs to do that and writing that "Google (essentially) says you don't need to" in their style guide is much less work. ;-)
The correct way is using ints, e.g. "int x = b[i] & 0xff;". Pretty much you should do math with int and long only. java.util.Integer/Long nowadays have static methods to work with unsigned types if you need them.
Once you remember that you need (the annoying) "0xff", you'll be free from errors. It has been there for 24 or so years. Other than that wrap your arrays into ByteBuffer and use provided utility.
The memory layout of the classes is quite immaterial here as the operation do happen within CPU registers.
[0]: https://docs.oracle.com/javase/specs/jvms/se12/html/index.ht... [1]: https://en.wikipedia.org/wiki/List_of_Java_bytecode_instruct...
static int square(int);
0: iload_0
1: iload_0
2: imul
3: ireturn
is java; compare to the c++ equivalent (used unsigned to avoid any possibility of undefined behavior): unsigned char square(unsigned char num) {
return num * num;
}
Which compiles to: mov eax, edi
imul eax, edi
ret
on x86 mul w0, w0, w0
ret
on arm.By this definition, c and c++ don't have "real" byte types either, because they undergo integer promotion for operations, and the generated code is operating on register sized values, not bytes. Java bytes use 8 bits as part of an object (with a fixed-cost padding); 8 bits as part of an array, etc. The fact that the bytecode has intermediate casts is not relevant to the actual code executed on CPU.
They're not actually operating on register-sized values: the code labelled x86 is also the x64 code, meaning GCC operates on 32b when the native registers are 64b. Likewise ARM.
Your assertion is even more debatable when Clang actually operates on 8-bit registers on x64:
mov eax, edi
mul al
ret
(it does not on ARM64 as `mul` is only defined for 32 and 64b)> At the end of the day, code is for people, so keep it easy for people.
This is a dangerous heuristic, because people here means exclusively developers. It’s easy for us to trade annoying but mostly harmless bugs for rare but harmful ones.
But "unsigned by default" won't save you here; all it does is give you a little more breathing room for your special case code (and hey, maybe that doubled positive integer space is all you needed, in which case congrats).
But all of the other problems are still there. That's why I'm saying that "unsigned by default" is backwards. It should be "signed by default", and unsigned for special situations that warrant it.
That Java has no support for unsigned integers is a very common complaint.
I totally agree!
After some horrible bugs caused by unsigned integers in innocent looking C/C++ code, I'm 100% in the signed camp. The only time I use unsigned is when I need well-defined overflow behavior (which is rare).
As a bonus, using signed loop counters in C/C++ code enables certain kinds of compiler optimizations (based on the fact that signed integer overflow is undefined behavior): http://blog.llvm.org/2011/05/what-every-c-programmer-should-...
A big one is dealing with currencies like Japanese yen in financial software.
The question is: Will the difference between 8 and 16 quintillion matter enough to say "unsigned by default"?
But currency types need to be signed anyway, so I guess that's that ;-)
Timestamp arithmetic is a common case where unsigned integers with silent overflow is superior. You can avoid bounds checks and special cases by always computing a delta between a newer time and an earlier time. As long as you can guarantee that the sample period is less than the clock rollover period there will never be any ambiguity.
Ada tried imposing the purity of signed integers and had to add modular types to get these semantics because of their utility.
http://www.gotw.ca/publications/c_family_interview.htm
Gosling on why Java only has signed arithmetic.
The only few cases you want that are e.g. hash functions, crypto, etc. In all the other cases it's a mistake, and the "patterns" shown in this article to circumvent the issues with unsigned just for the sake of using it are extremely ridiculous I think and much less readable than the normal code using signed integers.
If you have things that must never be negative, define a type that does that and gives an error (there are many good safe_int examples in C++) whenever an operation gives a negative number as in that case you've already lost and your business logic / input filtering / .... is wrong and needs to be fixed.
Anecdotally, I've written a few hundred thousand kloc of c++ so far and I've never ever had a bug due to signed overflow. Unsigned underflow OTOH... I'm just thankful for clang's ubsan to warn on it because of how many issues it caught. For the immense majority of programs you'll never have sizes close to 63 bits anyways since the CPUs we use barely have 52-bits of address space at most (and if you have more you're likely already using 128 bit sizes)
> 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.
I'd take negative indice over silently wrong indice any day of the week. The first will be caught hyper quickly, the second will send money to the wrong account silently for a couple years before anyone notices and then you're in much deeper issues.
Unsigned does mean non-negative (or rather no indication of negatives) as the sign of a number is just the positive or negative factor of a number: 1 or -1.
The standard idiom for doing a reverse loop would be: "for (int i = size; i-- > 0; ) ...". As a side benefit it has lower registry pressure. Some folks have issues reading that one as well.
Saying all that not being 'unsigned fan', just the popularity of the idioms comes with their use frequency, and given that 'unsigned; is not popular at all...
Signed is modular arithmetic with an offset.
An unsigned char called i can be thought of as "i mod 256"
A signed char called i can be thought of as "((i+128) mod 256) - 128"
All results for over/underflow hold for 'i' cast into a char if you do the above in both cases.
I think the premise of the article is quite reasonable. It's slightly easier to reason about over/underflow for unsigned arithmetic since it's not offset.
it is not, it models integers (Z). The model only works in the bounds of what the platform can offer of course ; when you use "int" you say "this is an integer. In any case I'm supposed to encounter, adding two positive integers will yield a greater positive integer ; if I add numbers so big that my platform cannot represent the sum my program is meaningless anyways".
It is very unlike unsigned which models (much more accurately since it's much easier for our computers with finite memory) Z/pZ.
The reason you see -2 printed out when you add signed chars -1 and -1 together is because adding 255 and 255 together gives 254 under mod 256. The signed model that allows negative numbers relies on the fact that all numbers on computers are modular arithmetic.
So in that sense any argument that unsigned is modular and signed isn't is incorrect. You're better off choosing the one that forces you to consider how the computer actually operates under the hood.
if you are writing code in C or C++ the computer you are programming for is the C / C++ abstract machine. C++ recently sanctified complement-of-two as integer representation, but C does not and supports machines with one's complement representation.
While that's true, signed integer overflow on arithmetic operations remains undefined behavior.
In which case it has a terrible name. The name clearly indicates that the primary feature is that it is not signed, and the logical conclusion from that is that it is not negative.
That said, I agree; using signed types in C/C++ does generally seem safer and a majority of the techniques listed in the article are unsafe, logically confused, and/or inhibit optimisation. Using signed types in languages like C# is also generally more pleasant, because otherwise you end up with a lot of casts just to interact with the base types (like arrays), language features and third-party libraries.
No, the logical conclusion is that since it has no sign, you don't know whether it's positive or negative. Otherwise the name would be "positive" or "non-negative", not "unsigned".
And this conclusion would also be correct.
Years ago, when the names were designated the CPUs didn't even have proper signed instructions. There was a sign flag, and that was all. In that regard the unsigned was the natural CPU sympathetic type.
FWIW, signed seems to be the default for most other languages from that time period, and often the only option - e.g. Algol-60 didn't have unsigned integers at all. Pascal kinda sorta did, if you defined an integer subtype with 0 as the lower bound... but the upper would still be that of signed int, and it'd behave as such. And Algol-68 called its unsigned type BITS - a pretty strong hint that they didn't think of it as arithmetic.
I'm actually kinda curious now as to when the concept of unsigned integers first appeared in a high-level programming language first.
Both signed and unsigned arithmetic are using finite fields, instead of Abelian rings as most people expect.
[1] https://biblio.ugent.be/publication/314490/file/452146.pdf
This is not correct. They are rings with some additional (not mathematically standard) operations thrown in.
'Finite field' means something very specific, and quite different. https://en.wikipedia.org/wiki/Finite_field
Computer integer arithmetic hardware can be used to implement finite field arithmetic, but even the 2^n case takes quite a bit of trickery. https://en.wikipedia.org/wiki/Finite_field_arithmetic
It does mean "not negative" in languages with bounds checks. Like Rust.
In Racket (and in mathematics) we call those natural numbers. The very notion of "unsigned" was invented by video game programmers at Bell Labs. In mathematics positive numbers are still signed (they just have a positive sign).
PL/I had (has) unsigned integers back in the 60s, picking up the idea from earlier languages. I believe the novel datatype in PL/I was the CHARACTER type; into the 80s I was still programming machines with variable length bytes.
In many ways PL/I heralded the current Ordovician stage of programming languages, with the enormous Cambrian flourishing of hardware-specific (and company-specific) languages slowly dying out. Interestingly the only surviving languages older than it, FORTRAN and LISP, are also machine-independent.
Yes: https://doc.rust-lang.org/std/primitive.usize.html
> The pointer-sized unsigned integer type.
> In Racket (and in mathematics) we call those natural numbers.
Naturals have no upper bound. Most languages don't have naturals, because they do have upper bounds. Languages which do provide naturals (limited by the host machine) usually don't bother with signing segregation, and give you mathematical integers (again limited by the host machine's capabilities).
Any language that offers access to features of the machine will support unsigned numeric types.
No language supports natural numbers. The best you can get is bignums. A language that calls its numbers natural is just lying. (Likewise, integers, and reals.)
"int" is not a lie, it is a hint.
Huh? If you write things[n] in Rust, and n isn't usize that's an error, you can't index things with the other primitive types at all out of the box.
Pretty much everything for indices (at least in the stdlib) seems to be 'usize' in my experience (i.e. Vec), which is unsigned.
Am I missing something?
Do you run your code with -fsanitize=undefined -fsanitize=integer ?
Note that when mathematicians work in the area of congruences, they do not use unsigned integers.
For instance, when we look at Euler's Theorem:
https://en.wikipedia.org/wiki/Euler%27s_theorem
all the quantities in the formula can be understood as just integers, not unsigned integers.
The exponentiation of a can be understood as regular exponentiation. The modularity plays out in the triple-equal-sign operator and the (mod n) parenthetical part which says that the two sides of the equation are equivalent in a particular way.
The left side of the equation uses ordinary exponentiation and can produce values >= n; it is not wrapped.
It's crystal clear that we cannot replace the triple equal sign with the regular one, and drop the (mod n).
Don't treat computer "integers" like mathematical integers. It leads to pain. They're members of a few finite fields.
This is something the Ada language gets right: you define your integer type with a range, and the compiler automagically inserts the runtime range checks. (Unless you switch them off with compiler directives.) It makes far more sense to bake the range-checks into the type than to do it the manual way and just hope you don't miss anything.
You can do this in C++ using a library (thanks to templates) [0] but this is very rarely done.
[0] https://www.boost.org/doc/libs/1_78_0/libs/safe_numerics/doc...
> non-parenthesized arithmetic operations could be re-ordered by the compiler, which may result in a failing computation (due to overflow checking) becoming a successful one, and vice-versa. By default, GNATprove evaluates all expressions left-to-right, like GNAT.
Presumably one solution would be to use a three-address-code style [2] to completely tie the compiler's hands regarding ordering, but this seems painfully restrictive even by the standards of SPARK.
Also, an Ada compiler's internal choice of base type (with which to implement a range-based integer type) can impact overflow behaviour: [1]
> The choice of base types influences in which cases intermediate overflows may be raised during computation. The choice made in GNATprove is the strictest one among existing compilers, as far as we know, which ensures that GNATprove’s analysis detects a superset of the overflows that may occur at run time.
I don't know if there's a fully portable robust answer to this second problem. (I believe you can generally use hints to force the Ada compiler to use a particular sized type, along the lines of C's uint32_t, but that this isn't portable.)
edit On second thought, I imagine using a three-address-code style would solve this too. Either the result falls within the permitted range of the destination variable, or it doesn't.
[0] https://docs.adacore.com/spark2014-docs/html/ug/en/appendix/...
[1] https://docs.adacore.com/spark2014-docs/html/ug/en/appendix/...
-- Range checked equivalent of uint64_t;
type Nibble is range 0 .. 15 with Size => 64;
-- Every assignment is as if Value := (Value % 16);
type Wrapped_Nibble is mod 16;
type Small_Float is digits 4 range 0.0 .. 20.0 with Size => 32;
I agree that the subexpression issue is problematic, IIRC I thought this was checked in SPARK code, but I could be wrong. The big benefit is describing your intent and you know that the stored value is in range, though whether it went outside of that range in calculation might not be known. Another thing is that these types define 'First, 'Last, 'Pred, 'Succ, and 'Range attributes which you can use. for N of Nibble'Range loop
-- do something
end loop;I believe that's correct, see my other comment. [0] Does still feel a bit ugly though.
When a signed integer is used as an array index, the value can be in one of three ranges: the <0 range, the >=0 && <size range, and the >=size range. To validate the index, you need two comparisons. When an unsigned integer is used as an array index, there are only two ranges: the <size range, and the >=size range, and you need only a single comparison.
And that's not the worst situation with signed integers. From a more general point of view, there are four "classes" of signed integer: positive (>0), zero, negative (<0), and INT_MIN. Everybody tends to forget about that last one, but it's "special" in that it can break things in unexpected ways. Negating it doesn't work (you'd expect negating any number less than zero to result in a number greater than zero, but for INT_MIN that doesn't happen). Dividing it can trap (see for instance https://kqueue.org/blog/2012/12/31/idiv-dos/ which has a couple of examples) or worse.
With unsigned integers, there are only two "classes", zero and non-zero, and it's common to not even need special treatment for zero, reducing the whole thing to a single "class" of values.
If your code doesn't have bugs, no validation of indexes is required.
Your code doesn't have bugs because you validated your indexes. What you said makes some sense when the index comes from within the program itself, but not when it comes from outside the program. Consider for instance the case of reading a data structure from a file or the network, where one field is an index or an offset into another part of the structure; you must validate that this index or offset is not outside the bounds, no matter how perfect your code is.
So I opened a ticket with Pixar [2].
Some of the same strange arguments were brought forward (incl. the Google style guide BS). I wrote a longish reply [3] that mentions two comments [4,5] from an ill-guided article promoting use of signed in C++.
I closed the ticket in the end because I felt that if people really believe there are good reasons for signed in the cases at hand they haven't grokked the problem sufficiently – trying to convince them otherwise would not be a good use of my time.
[1] https://crates.io/crates/opensubdiv-petite
[2] https://github.com/PixarAnimationStudios/OpenSubdiv/issues/1...
[3] https://github.com/PixarAnimationStudios/OpenSubdiv/issues/1...
[4] https://www.learncpp.com/cpp-tutorial/unsigned-integers-and-...
[5] https://www.learncpp.com/cpp-tutorial/unsigned-integers-and-...
> The argument with substracting 5 from 3 is not an argument against using unsigned. When you use signed you can run into a similar problem when the result underflows. This happens much less frequent and causes UB, at INT_MIN, which is the worst kind of bug. The bug is often not triggered durring testing and you will only notice it when it is too late.
The "When you use signed you can run into a similar problem" part is just not true in our world. 3 - 5 happens much more often than anything that may cause signed .*flow.
"signed underflow" "bug report"
yields 8 results on google FFSI agree with the WP article saying integers more correctly "wrap around" (than overflow).
I can't actually find a normative definition for "overflow" in the C or C++ standards, but there are sections and notes that describe this interpretation:
C++: https://eel.is/c++draft/full#basic.fundamental-note-2
C: http://web.archive.org/web/20211231011139/https://cigix.me/c...
There can be subtle differences between various definitions of "overflow", and I imagine there could be other subtly different definitions outside C and C++. But "underflow" is exclusively a floating point concept, as far as I know.
c1 = block[i1]; c2 = block[i2];
if (c1 != c2) return (c1 > c2);
i1++; i2++;
becomes mov cl, byte ptr [rdx + rcx]
cmp byte ptr [rdx + rax], cl
jne .LBB0_6
lea eax, [rdi + 2]
lea ecx, [rsi + 2]
if the indices are unsigned, but mov al, byte ptr [rcx + rdx + 1]
cmp byte ptr [rdi + rdx + 1], al
jne .LBB1_6
if the indices are signed, that is the compiler just removes the indices and traverses the block directly (starting at the input offsets).That's because in C (and C++) overflow is UB, so the compiler can assume it doesn't happen. Clang is apparently unable to make such a determination or work around it for unsigned, so for every increment of the indices it actually goes and computes the indices to fetch the items from the array.
Incidentally, GCC does not care and generates the exact same (rather different) code for both signednesses.
[reg1 + reg2 + disp]
reg2 refers to a signed integer (Which may result in the effective address being < reg1 if reg2 is negative).Obviously you would not want to make the same assumption if reg2 refers to an unsigned integer, because if the most significant bit is set, it would result in an effective address below reg1, which is definitely not what we would expect from adding an unsigned integer.
But in this case, 64-bit addressing is being used, and the value of reg2 is a 32-bit integer which has been specifically zero-extended. We can make the assumption that [reg1 + reg2 + disp] can never result in an effective address below reg1 (assuming positive disp).
If you take the signed version, and replace the lines
movsxd rdi, edi
movsxd rcx, esi
with movzx rdi, edi
movzx rcx, esi
I believe you will have something functionally equivalent to the unsigned version.Of course, this same optimization could not apply if we were using uint64, but it could in the case of int64.
Either way, buggy code with no bounds checking is not a reliable method of determining what optimizations can be done in production.
In the int32_t case, suppose i1=-1. Then you want to access block[0xffffffffffffffff], block[0x0000000000000000], block[0x0000000000000001], and so on. You can do an initial sign extension of the indexes, and then the base64+index64+disp32 addressing mode gives you the right result.
Make the unsigned version take uint64_t indices, and you get the same code for both. Or #include <stddef.h>, and make it take size_t - same thing. This is exactly the sort of thing that size_t is there for.
Then the common cases are fast and the edge cases slightly slower, but everything works the same.
Consider for example:
extern void g(int);
void f(int x, int y)
{
g(x - y);
}
The function f obviously doesn't work for all values of x and y: Some values will overflow, some values will underflow.How do you fix that? Writing an explicit check for whether x-y overflows is highly impractical, no one does that.
Or you could document the limitation using pre- and postconditions that narrow down the range of allowed values to something you know will work:
extern void g(int);
void f(int x, int y)
/*
PRECONDITION: -1000000 <= x <= 1000000 and -1000000 <= y <= 1000000
*/
{
g(x - y);
}
No one does that either. I don't know why. There's no principled reason why you couldn't meticulously keep track of signed int input and output ranges. But that's how it is, no one does it, valid int ranges are always unstated.Now for the unsigned version of the same problem:
extern void g1(unsigned);
extern void g2(unsigned);
void f(unsigned x, unsigned y)
{
if(x >= y)
g1(x - y);
else
g2(y - x);
}
Assuming g1 and g2 are well-defined for all unsigned inputs, so is f. That was easy.It may look like more code, because there are two g functions now. But chances are you probably need to treat the x<y case differently somewhere anyway; in the larger scheme of things, unsigned is not more code.
https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins...
It doesn't solve the problem though. You still need to write some code for what to do if there's an overflow, and you need separate handling for underflow. So the signed case is now:
extern void g1(int);
extern void g2(int);
extern void g3(int);
void f(int x, int y)
{
int x_minus_y;
if(__builtin_ssubl_overflow(x, y, &x_minus_y))
g1(x_minus_y);
else if(x > y)
g2(...); /* overflow */
else
g3(...); /* underflow */
}
Work in progress. I've given up on figuring out what arguments to pass to g2 and g3. Since the difference won't fit in an int, you would need to offset-adjust the value somehow in order to fit it in an int. Seems messy. Maybe you can think of something simpler.https://docs.microsoft.com/en-us/answers/questions/680467/ex...
Had Microsoft used an unsigned int to store the date - which can't ever be negative in this notation - Exchange wouldn't have stopped working worldwide on 1/1/2022. They would have been safe for another two decades, in which they would hopefully had either gotten rid of that weird code or switched it over to 64 bits.
I think using 64 bit integers for their natural hardware-accelerated sortability is quite a neat trick. You just have to be sure to use actual 64 bit numbers, and you have to think ahead to check if your data type is wide enough to actually contain your data.
Why? The code would have kept working all along, there would have been no reason to explore it, and lots of other issues to chase.
*to some definition of 'only' that excludes the cases like cryptography, hardware, etc stuff.
Suppose that a, b and c are small values close to zero and so can be combined additively/subtractively in any combination without overflow. If they are signed, we can rely on algebra, like:
a - b > c // this expression
a - b - c > 0 // can become this expression
a - c > b // or this
This breaks for unsigned; you cannot just change the sign of c and move it to the other side of the inequality.Even when you're not actually doing this algebra in the code, it's useful to be able to do it outside of the code. Sometimes in the code too.
For instance, suppose now that the values are not trivial. We have no assurance that a - b will not overflow, but we do have assurance from somewhere that a - c will not. The rewrite a - c > b is then helpful in avoiding overflow, and easily provable to be equivalent to the a - b > c expression that naturally comes from the problem specification.
if (y && x > (T)-1/y)
I think it's sarcasm, but with C guides like this it's never certain :)(1) integer overflow/underflow is undefined behaviour. You don't want that, it's evil. Unsigned does not have that problem (often it is still a bug if you over- or underflow -- but at least you get sane code from the compiler).
(2) size_t is unsigned and is returned by sizeof() and is the argument to memcpy(), strcpy(), etc., and with -Wconversion, you do not want to handle all the warnings when mixing this with signed loop counters or size variables.
Unfortunately, one problem with using unsigned by default in C is that ptr1-ptr2 yields ptrdiff_t, which is signed. Which is bad, in my opinion, because I usually want to find an array size or index when I subtract two pointers, so I want size_t, and in these cases, I always know which pointer is larger.
Oh well, it's C.
This of course has to do with me frequently writing and maintaining low-level hardware and network protocol code, talking to devices on the other end that had firmware written in C and therefore expect you to be fluent in bit manipulation. But hey, all that nice software is useless if it can't eventually interface with the real world out there, so hardware will stay relevant and necessary and the accompanying close-to-the-metal code can't be optimized out of the system, hence I consider good tools for doing bit-level work an important quality in any general-purpose programming language. And besides bit-level operators that means support for unsigned integers which are needed to easily combine bit-level with classic arithmetic operations.
You can actually live without either of these in practice and still do low-level stuff, but it is very unintuitive, has poor runtime performance and leads to hard-to-read and bug-prone code.
But Kotlin (also running on the jvm) got unsigned types with built in operator overloading for them, making it easy to work with. "val a = 100u" gives you an unsigned Int. If you use Java, there are few reasons not to use Kotlin. https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-u-int/
Are you referring to performance issues caused by lack of unsigned types? Do you have a specific example? Based on my observations, the conversions necessary for acting on signed types as if they were unsigned generates the correct machine instructions designed for unsigned types.
Also in general saying all we have to do is (I'm paraphrasing a bit) "not make mistakes" is, to me, a bad idea, as all programmers make mistakes.
I sometimes assert after a calculation that a value is >= 0, it's much harder to check that if it's underflowed because it was unsigned.
Am I alone here feeling that this while working is anything but intuitive and is asking for trouble down the road? Just imagine having to change the countdown for whatever reason in maintenance to 1 instead of zero and you suddenly fiddle with the operator.
for (size_t j = size; j > 0; --j) {
size_t i = j - 1;
// etc.
Or even: for (size_t j = 0; j <= size; ++j) {
size_t i = size - j;
// etc.However, its not true that unsigned is faster because the compiler can optimize. Consider:
x = (x * 2) / 2;
If x is unsigned, and overflows, x will not be the same after this operation so the compiler can not optimize away this line. If x is signed and overflow is undefined, the compiler can assume it wont overflow and optimize away the line. UB affords the compiler a lot of optimizations.
> There are some optimizations compilers can make assuming signed integers cannot underflow or overflow that unsigned does not get to participate in.
It’s not false either. For instance, casting int32_t into int64_t requires an instruction like movsxd which does sign extend. Casting uint32_t into uint64_t can often be merged into the instruction who computed the uint32_t value, because the instruction which write 4 bytes registers like eax zero out old data in the higher 4 bytes of the corresponding 8 bytes rax register.
You could argue that it doesn't matter because an incorrect program is incorrect regardless. True. At least with signed integers you can represent differences between positive values. How do you do that with unsigned?
Regarding differences, in practice you usually want those differences to be non-negative. With uints, you compare first. Not:
int length = end - begin;
if (length < 0) return error;
but: if (begin > end) return error;
unsigned length = end - begin;
This is also a good habit for signed ints, since it avoids underflow in more (but not all) cases.An old post with a few fun crashes for negative inputs: https://kqueue.org/blog/2012/12/31/idiv-dos/
"Use ints until you have a reason not to. Don't use unsigned unless you are fiddling with bit patterns, and never mix signed and unsigned.."
Of course uints overflow too, which just means arithmetic is hard, and it's often (not always) made harder when you have to consider negative values. Bounds checking, average, division, etc. INT_MIN is a horrible edge case which is easy to forget.
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1087...
https://github.com/teeworlds/teeworlds/commit/4d529dcd2d0102...
Here it tries to avoid overflow by assigning the result of 32 bit arithmetic to a 64 bit type. That's a common mistake.
Most signed/unsigned related bugs (outside numerical/math sensitive domain) are due to using both simultaneously and the side effect is of course going to be overflow/underflow.
On the other hand, I'd favor int over unsigned as it's more practical when dealing with memory address arithmetic & array indices due to address difference being signed.
But I sometimes wonder if it would have been better to not carry the 'signedness' in the variable type in high level languages, but instead treat integer variables as 'signless' bags of bits, and signed-vs-unsigned are just different views on those bags of bits, just as in assembly.
Most operations on two's-complement numbers result in the exact same bit pattern, no matter if the involved operands were 'signed' or 'unsigned', and the signedness only matters when printing the result, or for explicitely extending the sign bit when casting to a wider type.
You can make a zero-overhead analog of optional<int>, for such cases, for clarity and safety. (Unless you are also trying to present a C API for cross-language compatibility...) But it is tricky to determine the exact amount of handholding to provide for such a type, and extra work to remember exactly what it is. We all know how ints work.
> The argument is that unsigned is dangerous here because if y > x then you get underflow. The problem with this argument is it’s not valid because the code itself is simply incorrect regardless of the signedness of x and y. There are values for both x and y which will lead to signed integer underflow. So like before, in languages like C and C++, you just unconditionally invoked undefined behavior since signed integer underflow is undefined.
Yeah because x=1, y=2 is just as likely an input as x=MIN_SIGNED_INT, y=1.
make([]T, m, n) does not allocate a slice of length n*m, but of length n and capacity m.
- Loop using an iterator: unsigned pointer size
- Array index: unsigned pointer size
- Enum used to represent a register value that's 8 bits or less: unsigned 8 bit
- ... used to represent a register value that's up to 16 or 32 bits: unsigned 16 or 32 bit number
- Integer value that could be negative: Signed, with the appropriate size
- Doing numerical operations and you have an FPU: Float. Unless you're careful, you know what you're doing, and it's either performance critical or you're using dedicated a dedicated fixed-pointer hardware peiph... then maybe fixed point with a signed or unsigned integer.
- Analog value that can only be positive; maybe a voltage: unsigned integer according to precision of the ADC etc
- Analog value that can be positive or negative; maybe pH, audio, or something using a differential ADC: signed integer
- Performing mathematical operations including subtraction, don't expect fixed-point errors to come up, and you don't want to use floating point or don't have an FPU: signed integerThis sort of code is written very carefully and mistakes give completely wrong answers so they are noticed right away.
>use of signed integers is better as it’s the only way you can get trap behavior for integers, as using it on unsigned would trigger trap representations for valid code that relies on that behavior.
For video and audio codec code, and really anything integer math heavy, being able to use UBSan to find overflows is a hugely beneficial tool, and something you can normally only do with signed integers due to the C spec (it's less of an issue in Rust as that language makes both signed and unsigned overflow illegal).
>Trap representations are actually quite insufficient as they can only trigger at runtime when those paths are successfully executed with the correct trap-producing inputs. This coverage is impossible to expect in any non-trivial program even with exhaustive unit testing.
This was true before fuzzers existed, but now that we have several very good fuzzer implementations (plus a few smart unit tests), the coverage is well within reach.
No it doesn't.
Neither signed nor unsigned integers in WUFFS permit overflow or underflow, at runtime the integers behave the way you were taught in school. Adding two positive numbers together always results in another positive number, because duh. WUFFS catches all the cases this programmer worries about at compile time and insists you fix them.
One of the first examples WUFFS presents you is what if we take this correct code for a well known operation and we tweak it so that it can overflow. The compiler rejects the modified code, pointing out that it could now overflow and so isn't valid WUFFS.
Now, WUFFS is a special purpose language, you can't rewrite your 5MLOC C++ system in WUFFS. But then again, this shouldn't make you feel glad (because C++ is so powerful) but instead sad (because C++ can't offer you this valuable defence against your own stupidity). It should also make you determined to use WUFFS everywhere you possibly could, although doubtless it won't.
And that right there is a compelling reason not to use C or C++. If the language is so broken that it doesn't even let me subtract signed integers safely without risking shooting myself in the foot it is worse than useless in today's world because it gives me the illusion of programming in a high-level language when in fact I am not.
When I want to subtract x from y, I want to be able to write y-x and have the language worry about the details. That is the whole point of having a high level language, to abstract away the low-level details and let you write high-level abstractions without having to worry about introducing bugs that could imperil your entire enterprise. C and C++ don't do that.
There is never a reason to use C.
But C++ is a language for engineers. If you don't have the patience to do engineering, then you should not. If you have a need for engineering anyway, hire one.
There are no booby traps, only booby programmers.
The failure modes of C and C++ ints are less bad than is typical of construction materials. The common mistake is hiring CS people and expecting them to think and act like engineers.
If the subtraction problem were the only issue that might be manageable but it is not. It is just one example of a very long list of such problems. C and C++ are metaphorical minefields of gotchas, any one of which can result in a literal crash or a security breach, which in today's world can be every bit as damaging as a physical failure of a critical system. Actual examples of this occur on an almost daily basis. It is akin to trying to build a modern passenger aircraft out of wood and muslin. It really is unacceptable.
Engineering is a discipline that requires years of study and supervised practice, not reducible to "the art of making trade-offs". Non-engineers hired to perform engineers' jobs like to make this sort of mistake. Management might not be able to tell the difference. Thence 737-MAX.
https://en.wikipedia.org/wiki/Tu_quoque
To say nothing of the fact that your backhanded assertion is completely baseless. Also, C and C++ provide neither run-time safety nor compile-time safety, so you argument is a non-sequitur as well.
The correct default IMHO, is a proper numeric tower paired with a tagged pointer implementation for "small" integers (range in ~ ± 2^62 or so). As others have pointed out, this will essentially never overflow in practice, so memory traffic is the same as for 64 bit primitive integers. AFAICT, we are now good enough at optimising tagger pointer/small integer arithmetic that on modern machines, memory traffic should be the limiting factor.
I was vascillating between different options (for Objective-S, http://object.st) for a long time, because while I love C's "cheap and cheerful" use of int, but I also love Smalltalk's (and LISP etc.) "we do the right thing".
What pushed me over the edge towards the numeric tower as default position was DJB's retrospective paper in Qmail security: https://cr.yp.to/qmail/qmailsec-20071101.pdf
So while "cheap and cheerful" works really well in regular situations, it does not work well at all when you're under attack, because attackers will go 100% for the edge cases, the ones that "never happen". So we really need to, as DJB put it, "simplify integer semantics". Having those overflows just cause a small slowdown seems like a reasonable tradeoff, we don't need attacks to execute at optimal efficiency.
Given the advances we have in software and hardware, there simply is no reasonable case to be made for not having a proper numeric tower as default.
When you need better performance or specialised semantics, the primitive types should be readily available. But not the default.
I'll put it in my same mental bucket of "things that I no longer worry about", along with buffer overflows, wild pointers, etc that are eliminated by memory-safe languages.
> 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:
> [... division examples ...]
> INT_MAX - INT_MIN
That's not true: 'sub i32 INT_MAX, INT_MIN' is defined perfectly fine in LLVM in the (modular arithmetic) way you'd expect as long as the 'nsw' (no signed wrap) flag isn't set on the instruction. For the most part, LLVM IR by default doesn't care about the signedness of values, integer types are just treated as a bunch of bits with modular twos complement arithmetic. Exceptions to this rule are explicit, e.g. the mentioned nsw flag (and there's also nuw for "no unsigned wrap").
The division examples are correct because if you want a signed divide, you have to explicitly encode it as a signed divide instruction in LLVM IR, which has the listed undefined behaviors baked in.
uint8 a = 230;
uint8 b = 250;
auto c = a + b; // typeof(c) == uint9
Maybe it would even be able to detect "c / 2" fits in an uint8.I guess this could lead to an explosion of bits required, but only if you chain multiple operations and use type inference. If you explicitly assign to a fixed-width integer, it would truncate.
I know Python used bignums under the hood, but is there any compiled and/or strictly typed language that does this? Because it seems modulo arithmethic and silent under/overflow is most of the time not what you want.
Most numbers are small, and you aren't worried about overflow.
With big numbers, std::int64_t is hard to overflow. If you have numbers from an untrusted source, you can and should range-check them immediately on input.
And if the compiler gives you a warning... "Inferred variable would require a bignum (int128)" then you could either silence it by specifying the type explicitly, or maybe you are missing a manual range-check?
> If you have numbers from an untrusted source, you can and should range-check them immediately on input.
You should, but you are going to forget in some places. The point of a stongly-typed language to me is that the compiler will make sure you do. It is the edge cases and the cases where you accidentially don't sanitize properly where you get evil bugs.
What will they do instead? We don't need to guess, we saw in the first Ariane 5: catastrophic loss of vehicle and payload, as a result of sending a crash dump report to the engine steering gimbals. $500M payload, in that case.
This was perfectly reasonable back when RAM was measured in kilobytes, and clock speed in megahertz. But these days, we write apps in HTML and JS, and package them with a browser to run. And, conversely, security is much more of a problem than it was decades ago - and integer overflow is a very common cause of security issues.
So, why aren't unbounded integers becoming the norm? I'm not saying we should throw int32 etc out, but rather treat them as low-level optimization tools - much like we treat, say, raw pointers in C# or Rust. Surely the default should be safety over performance?
A small improvement in correctness is washed out by a huge decrease in ergonomics.
Where things get a bit more tricky is when working with data that has some native unsigned type - e.g. when reading an image from file, it will mostly consist of unsigned chars. There you also need conversion rules and everyone aligned on algorithms, APIs etc.
Mostly though it's a matter of maintainability - it's easier to wrap your head around signed integers (no pun intended), even for beginners.
When working in an untyped environment, this sort of question doesn't come up. If the value is intrinsically unsigned then that is what you and hence the program assume those bits mean. If the value is intrinsically signed then that is your illusion. Any ideas about overflow are entirely in your mind. You tend to see techniques used in an untyped environment that would never occur to anyone in a signed environment. It's all just bits.
I think a lot of the scenarios where signed is safer are also situations where the application logic would be wrong, so I am a bit skeptical of that argument.
But Ultimately the biggest factor to consider is what your team or prospective team members in your industry are used to, different industries tend to have people who never expect unsigned values vs those who always assume unsigned will be used.
No, it's not. Overflow on arithmetic is undefined, but on conversion it's implementation defined. In C++20 it's defined.
> These are certain to produce invalid results in languages like Go, Rust, and Odin.
This is clearly untrue, since an operation (or indeed a type) at the language level need not translate to the same operation/type within LLVM.
For example, none of these are "undefined behaviour" in Rust.
for (size_t i = size; i > 0; i--) { // ... }
and use i-1 inside the loop instead of using underflow?
Problem 1:
If i is used multiple times inside the loop, then you might need to make a temporary variable for it, making it confusing:
for (size_t i_plus_one = size; i_plus_one > 0; i_plus_one--) {
size_t i = i_plus_one - 1;
f(i);
g(i);
h(i);
...
(The alternative is a bunch of i-1 all over the place, which is also confusing.)
Problem 2:
If operations inside the loop is short, then the extra "i-1" arithmetic could result in unacceptable performance penalties.
What a great article.