# Signed vs Unsigned Sizes

Whether a language's default integer type for sizes, lengths, and indices is signed or unsigned. C made it unsigned (`size_t`) and most successors copied; Java dropped unsigned types entirely; Go picked signed; [[c3-lang|C3]] flipped from unsigned to signed in 2026 after [[unsigned-sizes-c3-mistake|five years on the unsigned default]].

The decision propagates through the type system: unsigned sizes mean unsigned indexing, which means unsigned arithmetic in any code that touches array lengths, which means signed-unsigned mixing everywhere, which forces the language into either implicit conversion (silent bugs) or pervasive casting (cast-spam culture).

## Bug classes the unsigned default enables

- **Reverse-counting infinite loop.** `for (uint x = 10; x >= 0; x--)` — the comparison can never be false. The pattern is so common that C3 special-cased `>= 0` as a compile error for unsigned types.
- **Signed-unsigned comparison flip.** `uint a = 0; int b = -1; if (a > b)` — C promotes to unsigned, so `b` becomes a huge positive number and the comparison is wrong. Languages that kept implicit conversion need an explicit "safe-comparison" rule to fix this.
- **Plausible-looking overflow.** Signed overflow produces an invalid-looking negative number; unsigned overflow produces a plausible-looking positive number that's just wrong. The unsigned case survives casual testing.
- **Ring-buffer wrap.** `((start - offset_back) % length + length) % length` — the natural negative-offset wrap pattern is silently broken with unsigned values, and no type-system rule can flag it. The correct unsigned form is uglier and less natural.
- **Modulo and division asymmetry.** `(foo + a) % 2` gives one answer when `foo` is signed and a different one when it's promoted to unsigned. Even with safe-conversion rules, `/` and `%` expose the underlying signedness in a way `+`, `-`, `*` don't.

## Bug classes the signed default enables

- **Negative array index attempts.** Caught at runtime as bounds-check failures; the language can't catch them statically without a richer type system.
- **Off-by-one near zero.** Possible, but the failure mode is usually a clean negative number rather than a wrap-around to a billion.
- **Theoretical range loss.** Half the representable range goes to negative values you mostly don't use. On 64-bit, you exhaust memory long before reaching 2^63 — so the lost range doesn't bite. On 32-bit, indexing past 2 GB is rare enough that languages like Go just accept it.

## Who picked what

| Language | Sizes | Notes |
|----------|-------|-------|
| C | unsigned (`size_t`) | The choice that propagated |
| C++ | unsigned | inherited from C; `signed` indexing has been argued for by Stroustrup and Sutter |
| Rust | unsigned (`usize`) | overflow is a runtime error, not wrap |
| [[zig\|Zig]] | unsigned (`usize`) | same convention as Rust |
| Java | only signed | no unsigned types at all |
| Go | signed (`int`) | by the team that knew C++'s pain firsthand |
| [[c3-lang\|C3]] | signed (`sz`) since 0.8.0 | switched in 2026 after 5 years on unsigned |

## The cast-spam vs minimize-casts dichotomy

A separate but linked design choice: when signed and unsigned types meet, does the language insert silent conversions or require explicit casts?

- **Cast-spam approach.** Casts are cheap to write, "they document the conversion." Downside: a cast that was correct when written becomes silently truncating when an upstream type changes. Casts become "silence all warnings."
- **Minimize-casts approach.** Casts mean "here be dragons." Cleaner in principle, harder to define rules for. C3 took this side, which forced the unsigned-sizes problem to bite — because unsigned sizes mean signed-unsigned conversion is constantly needed, and a minimize-casts language has to make most of those conversions implicit.

The Rust experience is illustrative: unsigned sizes plus an "explicit conversions everywhere" stance produces a codebase full of `as usize` and `as i64` calls. Mechanically inserted, they accumulate, and they don't actually catch the bugs they were supposed to document.

## The "unsigned encodes a precondition" argument

The intuition behind unsigned-by-default is that "this value can't be negative" is a useful precondition — making the type unsigned encodes it in the type system. This argument doesn't survive contact with verification tools: unsigned types encode "this wraps modulo 2^N," not "this is in [0, N)." The precondition was always an informal convention, and the type-system "guarantee" it provides is the wrong guarantee.

## The internalized-cost observation

The most interesting non-technical claim from the C3 retrospective is that experienced C/C++ programmers stop seeing the cost of unsigned-everywhere — they pay the cognitive overhead automatically and forget it's there. This is the [[peril-of-laziness-lost|laziness-as-virtue]] argument applied in reverse: the discipline that compensates for a tool's defect becomes invisible, and removing the defect feels like removing the discipline.

## See also

- [[unsigned-sizes-c3-mistake]] — the canonical worked example of this concept
- [[c3-lang]] — current language case study
- [[zig]], [[zig-functional-programmers]] — Zig kept unsigned sizes; useful contrast
- [[type-systems-vocabulary]] — adjacent debate about type-system terminology
- [[no-silver-bullet]] — language-design tradeoffs in general
