7 ms·
Almost Always Unsigned
- pmarreck 4mo agoRelated: 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 https://github.com/pmarreck/BLIP
- dataflow 4mo 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?
- nulltrace 4mo agoThat loop reads like a bug to anyone who hasn't memorized the wrapping rules. while (i-- > 0) on a signed index does the same thing.
- theokrueger 4mo ago[dead]
- AlotOfReading 4mo agoI really don't see what's supposedly awful about that loop, but if you want to count down to x instead of 0 you just do: for (size_t i = size - 1; i >= x; i--)
- dataflow 4mo ago> I really don't see what's supposedly awful about that loop The stopping condition is incredibly confusing and non-obvious. Misleading at first glance, in fact. The whole thing is so unidiomatic that I don't think I've even seen it once in my life. It's a better contender for an underhanded C++ code contest than production code. > but if you want to count down to x instead of 0 you just do i >= x No you can't. That fails if x == 0. Which perfectly illustrates why using unsigned everywhere isn't so great. And I say this as someone who likes unsigned types and uses them more than average!
- wasmperson 4mo agoThinking of it as a "stopping condition" is backwards, that part of the loop is called the invariant: https://en.wikipedia.org/wiki/Loop_invariant https://en.wikipedia.org/wiki/Loop_invariant You should think of it as the condition that's true for all iterations, not a one-time event that halts the loop. The loop is short for this: for(size_t i = size - 1; 0 <= i && i < size; i--){ } Which works for both signed and unsigned numbers. It just so happens that for unsigned numbers you can omit the left-hand side of the &&, and for signed numbers you can omit the right-hand side. To support arbitrary lower bounds, you omit neither.
- dataflow 4mo ago> You should think of it as the condition that's true for all iterations, not a one-time event that halts the loop. (a) It's "a" condition that holds true for all iterations, not "the" condition. Plenty of other conditions can hold true across the iterations of any given loop too. In fact the one interesting thing about the loop invariant compared to any other conditions is the very fact that it is guaranteed to cease to hold immediately after the loop, assuming you don't break in the loop. Other conditions can still continue to hold. i.e. The stopping condition is the entire point of the loop invariant. (b) I'm well aware what a loop invariant is; I've worked on compilers. I am also a human. Humans care about when a loop starts and stops. There's nothing backwards about it, it's the most straightforward way people think of loops. And it's literally why more modern languages have introduced better syntaxes that merely spell the boundaries and/or values, and skip spelling the invariants entirely.
- StellarScience 4mo ago> I really don't see what's supposedly awful about that loop Exactly! That's precisely the problem with it. (Hint: think about your code when size = 0.)
- layer8 4mo agoIt works perfectly fine for size = 0?
- xigoi 4mo agoThey meant x = 0.
- layer8 4mo agoNot sure if they meant that, since the “awful” was referring to the original loop that had no x, but the generic solution in that case is: for (size_t i = size; i—- > x;)
- StellarScience 4mo ago[dead]
- StellarScience 4mo ago> for (size_t i = size - 1; i < size; i--) Agreed, seeing that example briefly made me consider whether this blog post was a parody. Sure, it works for this exact example, by relying on i wrapping "down" to MAX_INT on the last iteration. But how long will it take the next developer who works on the code base to figure that out? Will they figure it out before or after committing changes that break it? Or worse yet, before or after shipping code?
- amavect 4mo agoPost-increment inverts to pre-decrement, but for-loops don't support proper syntax sugar for pre-decrement. for(size_t i = 0; i < size; i++){ // loop body } for(size_t i = size; i > 0;){ i--; // loop body }
- teo_zero 4mo agoPut the "--" in the condition. It's less ugly.
- amavect 4mo agoPedantically, 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.
- IshKebab 4mo agoI 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.)
- scared_together 4mo agoThere was a related article from the other side of the debate a while back: https://news.ycombinator.com/item?id=47989154 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?
- tialaramex 4mo agoI mean, Rust calls this function you want abs_diff let a = 1234_u32; let b = 5678_u32; let c = a.abs_diff(b); // Or equivalently u32::abs_diff(a, b); Some languages hate offering convenience functions, in particular in a language like C or one of the would-be C-replacements, those functions just clog up the same namespace as everything else, so having 100 intrinsic arithmetic functions for integers feels disproportionate. The C-like languages, even those which do have methods, often forbid methods on their "built-in" or "core" types like integers so they can't do what Rust did here. Edited: tweaked language
- jandrewrogers 4mo agoThe issue is that you need dozens of minor variants for the myriad cases involving integers, especially in C. That namespace becomes rather busy. This particular idiom for finding the absolute difference of an unsigned integer pair is pretty clean and rarely needed. In most contexts you know that one argument will always be greater than or equal to the other, so a simple subtraction will do.
- xyzzyz 4mo agoFor the stuff I do, I rarely use signed types. I find the example of finding difference between two numbers to be really silly. I don’t recall ever having to do anything like that, for a simple reason: I design my code so that I always know which number will be larger than the other. Let’s actually talk more about this max - min example. Let’s say you calculated this delta. What is it useful for in your code? Typically, in real algorithms, what you want is to not only know the delta, but also which number is larger.
- tialaramex 4mo agoShould 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.
- gumby 4mo ago> for instance C and C++ leave signed integer wrap undefined Not in C++29, and I think the big 3 compilers (gcc, clang, MS) will have squashed this long before that version is officially approved.
- tialaramex 4mo ago> Not in C++29 C++ 29 doesn't yet exist. It'll be finalised in (as its name suggests) 2029. Are you referring to some proposal you think has been accepted? Or is this one of those "I have concepts of a proposal?" ideas where you confused wishful thinking with reality ?
- gumby 4mo agoThe "list of undefined behaviour" annex has been written and was discussed at the last meeting. As I said, some of what's in it are already being implemented by the big compilers (though TBF to your comment not yet in any released versions as far as I know). But this case will be one of the early ones.
- theokrueger 4mo ago[dead]
- dataflown 4mo agoStroustrup recommends int over unsigned. Dijkstra recommends int over unsigned. Google coding guidelines recommend int over unsigned. Blogger recommends unsigned over int. Tough choice.
- StellarScience 4mo agoI 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 https://www.youtube.com/watch?v=Puio5dly9N8#t=12m12s - 42:40-45:26 - https://www.youtube.com/watch?v=Puio5dly9N8#t=42m40s https://www.youtube.com/watch?v=Puio5dly9N8#t=42m40s - 1:02:50-1:03:15 - https://www.youtube.com/watch?v=Puio5dly9N8#t=1h2m50s https://www.youtube.com/watch?v=Puio5dly9N8#t=1h2m50s
- nazcan 4mo agoThanks 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 fixed in the language. 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".
- Groxx 4mo agothat argument (and many others) also round to essentially "implicit imprecise integer casting is surprising but we do it anyway" which is... perhaps the actual problem??? I've made lints for Go that simply disallow implicit number casting, and omfg the (real, occurring but unnoticed) bugs it found. those kinds of lints are trivial to build, you can just stop doing it. forcing visible casts made many of these problematic patterns extremely suspicious at a glance, catching issues at review time far more easily.
- cpeterso 4mo agoThere 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 https://github.com/golang/go/issues/19623
- Groxx 4mo agoas much as I'm not really a fan of Go, and I would love to spend some time in a language that defaults to unsigned ints by default to see just how pleasant or painful it really is, I do think Go makes a reasonable tradeoff here. signed ints are rather obviously the majority decision, so that default is defensible, and their const / compile-time math is arbitrary precision. so as long as you can describe your math as either wholly const or can move all overflow-risky stuff into a const equation, you're good - Go's compiler will fail if it exceeds the bounds of whatever number you try to cast it to (implicitly or explicitly). ignoring fun with float rounding, of course.
- IshKebab 4mo agoIMO 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.
- bogdanoff_2 4mo agoYou can count down to zero with while(i--)
- pikuseru 4mo agoWhat if I’m making a 2d game and need to go left
- jgtrosh 4mo 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!
- johannes1234321 4mo ago> btw. Segmentation faults are your friends! Indeed. I can't count how often I told people that segmentation faults are among the best kind of errors. One has the complete state and not a log message missing 90% of relevant info. Of course you don't want processes to crash all the time and spend time on writing gigabytes of data onto disk. But for the rare failure it's a good thing to have.
- karussell 4mo agoAs a Java developer I'm a bit sad that we don't have a choice :)
- rehevkor5 4mo agoYou can do stuff with unsigned values, it's just awkward. It most frequently comes up when serializing things.
- pjmlp 4mo agoOr talking to the hardware, or doing graphics stuff. But it isn't the end of the world, a few helper methods and done with it.
- pjmlp 4mo agoThe reason is in this interview, http://www.gotw.ca/publications/c_family_interview.htm http://www.gotw.ca/publications/c_family_interview.htm > Quiz any C developer about unsigned, and pretty soon you discover that almost no C developers actually understand what goes on with unsigned, what unsigned arithmetic is. Things like that made C complex. The language part of Java is, I think, pretty simple. The libraries you have to look up. You have the option in the standard library, all numeric classes have unsigned arithmetic methods. And as we are finally getting value classes in Java (first integration steps), I expect someone will come up unsigned wrapper classes rather quickly, although it will be rather verbose without operator overloading.
- fpoling 4mo agoThe 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.
- rstuart4133 4mo agoNot disagreeing about the need for sentinel values, but Rust also has NonZero. When combined with Option, you get a similar result to outcome to C's -1. Similar because it's a compiler hack, as in the compiler treats std::num::NonZero specially. You can't create your own type with the same properties as you can in C.
- Asooka 4mo agoThe 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.
- delta_p_delta_x 4mo agoI 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.
- flohofwoe 4mo agoLoop 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 rule of thumb is to always use signed integers, except for bit twiddling and modulo-math. Especially with 64-bit integers it's really no longer an issue to lose one bit for the sign (63 bits ought to be enough for anybody heh).
- safercplusplus 4mo agoBut 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/doc/html/index.html https://www.boost.org/doc/libs/develop/libs/safe_numerics/do... [1] https://github.com/duneroadrunner/SaferCPlusPlus/blob/master/README.md#primitives https://github.com/duneroadrunner/SaferCPlusPlus/blob/master...
- kazinator 4mo agoIn 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.
- amavect 4mo agoI 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: Ensure INT_MIN ≤ y+x and y+x ≤ INT_MAX. To avoid overflow or underflow, first compare x to 0. In the case x≤0, INT_MIN-x cannot underflow, and y+x cannot overflow. If y compares greater than INT_MIN-x, then y+x evaluates correctly. In the case 0≤x, then INT_MAX-x cannot overflow, and y+x cannot underflow. And if y compares less than INT_MAX-x, then y+x evaluates correctly. ∀x∀y((x≤0 ∧ INT_MIN-x≤y)∨(0≤x ∧ y≤INT_MAX-x) → ∃z(z = y-x)) Ensure INT_MIN ≤ y-x and y-x ≤ INT_MAX. To avoid overflow or underflow, first compare x to 0. In the case 0≤x, INT_MIN+x cannot underflow, and y-x cannot overflow. If y compares greater than INT_MIN+x, then y-x evaluates correctly. In the case x≤0, INT_MAX+x cannot overflow, and y-x cannot underflow. If y compares less than INT_MAX+x, then y-x evaluates correctly. ∀x∀y((0≤x ∧ INT_MIN-x≤y)∨(x≤0 ∧ y≤INT_MAX+x) → ∃z(z = y-x)) The programmers that prefer unsigned arithmetic intuitively feel the greater simplicity compared to signed integers, but without any theorem proving, I agree that your assumption of small integers strongly supports signed integers.
- lerno 4mo agoCounterpoint: https://news.ycombinator.com/item?id=47989154 https://news.ycombinator.com/item?id=47989154
- shaggie76 4mo agoWhile 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 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)
- orlp 4mo agoThat's a GCC skill issue. You can do it in five branchless instructions for unsigned by splitting the unsigned up in two 32-bit halves, converting those to floats simply by inserting their values as mantissa into constants 2^52 and 2^(52 + 32). This conversion is exact. Then to finish the conversion you subtract 2^52 and 2^(52 + 32) respectively from the halves and add them together. vmovq xmm0, rdi vpunpckldq xmm0, xmm0, xmmword ptr [rip + .CONST1] vsubpd xmm0, xmm0, xmmword ptr [rip + .CONST2] vshufpd xmm1, xmm0, xmm0, 1 vaddsd xmm0, xmm1, xmm0 Here CONST1 = [0x43300000, 0x45300000, 0, 0] and CONST2 = [0, 0x43300000, 0, 0x45300000].
- Dylan16807 4mo 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.
- glitchc 4mo agoSometimes I need the 5th element of something, regardless of what it is. Sometimes the 5th element of something needs to be compared to the 35th element of something else. Both cases require the use of direct and/or derived indexing. This is often the case in low-level code. As for why signed is default, this may have to do with the error handling in C. By convention, if a called function hits a failure mode, it returns a negative integer. If a function succeeds but with a caveat or warning, it returns a positive integer. Unqualified success returns zero. Hence why idiomatic C functions typically return int, not unsigned int, and we've been stuck on this convention ever since.
- kps 4mo agoIt's a bit simpler. C didn't have `unsigned` until 1976.
- Narishma 4mo ago(2022)
- Panzerschrek 4mo agoThere are also performance benefits of unsigned integer types: division, modulo and sometimes multiplication for them works slightly faster.
- pjmlp 4mo ago> The most typical argument against the use of unsigned integers is that it’s more error prone Not only, most people don't know the rules even. > For me as a language designer, which I don't really count myself as these days, what "simple" really ended up meaning was could I expect J. Random Developer to hold the spec in his head. That definition says that, for instance, Java isn't -- and in fact a lot of these languages end up with a lot of corner cases, things that nobody really understands. Quiz any C developer about unsigned, and pretty soon you discover that almost no C developers actually understand what goes on with unsigned, what unsigned arithmetic is. Things like that made C complex. The language part of Java is, I think, pretty simple. The libraries you have to look up. Taken from http://www.gotw.ca/publications/c_family_interview.htm http://www.gotw.ca/publications/c_family_interview.htm
- Nevermark 4mo agousing 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;
- uecker 4mo agoA long time ago, I also thought one should use unsigned mostly but I am now in the opposite camp. Unsigned integers in C have semantics for modulo arithmetic. They are suitable if you need this, so for crypto, hashes, or if you only care about bits, etc. IMHO they should not be used for anything else. The reason is that it is very easy to screen for signed overflow bugs exactly because they have undefined behavior, just by turning on a sanitizer. It is also possible to transform overflow to safe traps at run-time where this is important. In contrast, finding unsigned wraparound bugs is extremely hard and can not be done automatically and preventing consequences of such bugs is difficult. Also any kind of index computation may have intermediate results that may be negative, so signed arithmetic is also generally more useful and far easier for people to understand and get right.
- Chu4eeno 4mo agoYou can also just add -fwrapv.
- variadix 4mo agoOne 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.