<p>I’m trying to decide whether my language should provide signed or unsigned bytes, but I’m struggling to find a good reason to choose either side.</p>
<p>Popular languages vary in their decision. For example, Java uses signed bytes, although they have unsigned shorts. C++ has unsigned bytes (<a href="https://en.cppreference.com/w/cpp/types/byte" rel="noreferrer">std::byte</a>), but I've seen a lot of code using signed chars as bytes.</p>
<p>Is there any consensus view on whether bytes ought to be signed or unsigned by default? Answers should ideally provide citations to back up their claims.</p>

<language-design><standard-library><integers>
Should bytes be signed?
CC BY-SA 4.0
2023-12-10T06:57:52.733Z
@RydwolfPrograms the question is meant the other way round. It's `type byte := i8` or `type byte := u8`? The language has both types already irrespective of the byte type.
feldentm
CC BY-SA 4.0
2023-12-11T20:55:28.273Z
**Comments have been [moved to chat](https://chat.stackexchange.com/rooms/150275/discussion-on-question-by-feldentm-should-bytes-be-signed); please do not continue the discussion here.** Before posting a comment below this one, please review the [purposes of comments](/help/privileges/comment). Comments that do not request clarification or suggest improvements usually belong as an [answer](/help/how-to-answer), on [meta], or in [chat]. Comments continuing discussion may be removed.
Michael Homer
CC BY-SA 4.0
2023-12-12T07:33:39.067Z
C++'s primary byte types isn't `std::byte`, it's `char` which is implementation defined as to whether it is signed or unsigned; and `std::byte` isn't really unsigned or signed.
Jack Aidley
CC BY-SA 4.0
2023-12-12T08:59:22.643Z
(A comment as I'm not yet able to post answers to this Q) You should maybe have a look at more recent languages which may have learned a bit from the mistakes of the past. For example, Rust has a fairly symmetrical set of explicitly signed and unsigned integer types for the various bit lengths. There are u8 and i8 (unsigned and signed 8-bit integers) as well as u16/i16, u32/i32, u64/i64 and u128/i128.
Hans-Martin Mosner
CC BY-SA 4.0
2023-12-12T17:05:38.233Z
@Hans-MartinMosner The language has signed and unsigned for arbitrary fixed sizes. The question is specifically about the concept of bytes. While I thought that the solution would be an alias like `byte := i8` or `byte := u8` in your terminology, I guess Matthieu convinced me that this might not be a good idea.
feldentm
<p>Signed bytes make bit-packing difficult, because when a signed byte is converted to a wider integer type, <a href="https://en.wikipedia.org/wiki/Sign_extension" rel="nofollow noreferrer">sign extension</a> means that the wider integer might use more than just the low 8 bits. Consider packing e.g. an IPv4 address into a 32-bit integer: the signed version needs <code>&amp; 0xFF</code> to unset the sign-extended bits.</p>
<pre class="lang-rust prettyprint-override"><code>fn pack_ipv4(a: u8, b: u8, c: u8, d: u8) -&gt; u32 {
    (a as u32) &lt;&lt; 24
    | (b as u32) &lt;&lt; 16
    | (c as u32) &lt;&lt; 8
    | (d as u32)
}

fn pack_ipv4_signed(a: i8, b: i8, c: i8, d: i8) -&gt; u32 {
    (a as u32 &amp; 0xFF) &lt;&lt; 24
    | (b as u32 &amp; 0xFF) &lt;&lt; 16
    | (c as u32 &amp; 0xFF) &lt;&lt; 8
    | (d as u32 &amp; 0xFF)
}
</code></pre>
<p>The main reason to have bytes at all is their compact representation. So it should be as easy as possible to convert between the packed and unpacked representation. Unsigned bytes are better for this, since conversions don't involve sign extension.</p>

CC BY-SA 4.0
2023-12-10T13:19:21.157Z
2024-01-23T18:02:42.750Z
2024-01-23T18:02:42.750Z
kaya3
CC BY-SA 4.0
2023-12-10T13:23:22.837Z
Making the signed version output a `i32` might be a more fair comparison, since you wouldn't need to set the sign for the leftmost byte
mousetail 'he-him'
CC BY-SA 4.0
2023-12-10T13:25:35.950Z
@mousetail Well, you wouldn't need the `& 0xFF` for the left-most byte either way, because the shift will drop all of the sign-extended bits. But personally, either way, I would write it this way because it's more obviously correct. The alternative would be to omit the one `& 0xFF` and write a comment explaining why it's not needed.
kaya3
<h2>Bytes should be unsigned</h2>
<p>There are a few reasons most languages have unsigned bytes by default:</p>
<ul>
<li>A more useful range. Bytes can only represent 256 different numbers so using them effectively is extra important. It is more common to want to represent a bigger number like 200 than -1, so the 0-255 is more practical than -128-127.</li>
</ul>
<p>This isn't really an issue for bigger numbers that usually default to signed. Even an signed short can go up to more than 30,000 which is not something you would typically count to.</p>
<ul>
<li>Bytes are most often used as a sequence, rather than as an actual number. You rarely use a type like a byte to store numbers intended as numbers, more often they represent bits of binary data, characters etc.</li>
</ul>
<p>The &quot;numeric properties&quot; only get in the way of accessing the raw data. Adding a special meaning to the upper half of the byte space would just add extra complexity to any serialization, parsing, string manipulation, etc. code that may want to operate on byte sequences.</p>

CC BY-SA 4.0
2023-12-09T22:29:26.023Z
2023-12-12T13:35:44.040Z
2023-12-12T13:35:44.040Z
mousetail 'he-him'
CC BY-SA 4.0
2023-12-11T18:15:44.453Z
I'd like to suggest one additional reason to keep them unsigned (this isn't enough to warrant a separate answer): it makes the hex representation consistent. For example, Java's bytes are signed, which means you can't do `byte b = 0xff` (that's out of range). But if you have `byte b = -1` and then view it as hex, that becomes `0xff`. This is true of any signed type, but it's very common to view bytes as hex, so it's especially noticeable there. (This is basically a special scenario within your point that "bytes are most often used as a sequence, rather than as an actual number.")
yshavit
CC BY-SA 4.0
2023-12-12T13:26:11.870Z
I think you meant to say "Even a **signed** short can go up to more than 30,000", since you're making the point that even 32767 is often enough, not needing UINT16_MAX 65535.
Peter Cordes
CC BY-SA 4.0
2023-12-12T23:50:31.687Z
@yshavit: I would think hex notation could be improved by saying that 0y123 would represent a number whose bits are all set except for the last 12 bits which would represent the bit pattern 0x123.  The number below zero would in hex thus be represented interchangeably as 0yFF, 0yFFFF, 0yFFFFFFFF, etc., without needing to treat the number of leading F's as significant.
supercat
CC BY-SA 4.0
2023-12-13T01:01:41.660Z
@supercat I'm not quite sure what you mean, sorry. Are you suggesting an alternate form of representing hex digits, that basically represents just the negative-int space of twos complement? If so, I think the thing is that very often when you're representing hex, you're actually thinking of it as a bitset — not as an int, signed or otherwise (html color codes are one obvious exception). But having the underlying data type be an unsigned int maps a bit better to that bitset perspective than a signed int would.
yshavit
CC BY-SA 4.0
2023-12-13T04:31:27.793Z
@yshavit: The idea would be that `0y` followed by digits would yield the signed value that starts with an infinite number of `F`'s followed by the specified value.  Especially useful when masking the bottom bits of a number, and one wants to leave the upper bits alone.
supercat
CC BY-SA 4.0
2024-01-18T18:37:09.587Z
I realise I'm late to this, but that would be confusing when `0y00` and `0y0000` don't mean the same thing, given that `0x00` and `0x0000` do.
kaya3
CC BY-SA 4.0
2024-01-24T02:19:44.687Z
Bytes are also commonly used to store enumeration values, since it's rare that an enumeration would require more than 256 cases. For enumerations, a numeric value at all is usually no more than an implementation detail; but caring about the ability to make those values negative is even less useful.
Karl Knechtel
<p>If they're signed, then you might have to worry about sign representation issues.</p>
<p>If they're unsigned by default, then their set of values will always have a consistent mapping to their bit pattern, regardless the sign representation.</p>
<p>But this almost doesn't even matter, because Two's Complement is the prevailing sign representation, and others are seldom, if ever, seen, especially in 2023.</p>

CC BY-SA 4.0
2023-12-09T22:09:32.523Z
2023-12-09T22:09:32.523Z
user1345541
<p>This still boils down to &quot;what do you intend to do with them?&quot;</p>
<p>As others have noted, sign extension is a nuisance when deserializing a wider integer, which suggests that unsigned is more useful.</p>
<p>Conversely, if <code>x</code> is signed, <code>x&lt;0</code> will tell you whether its top bit is set without having to know how wide it is.</p>
<p>I'm not convinced by arguments over the semantics imposed on the bytes; everything is a bunch of bits if you look closely enough, and arithmetic operations can be useful low-level operations on things that are not semantically &quot;numbers&quot;.</p>
<p>Even the idea of &quot;number&quot; is a conflation of several things, which ideally a type system would keep distinct:</p>
<ul>
<li>identifiers</li>
<li>ordinals</li>
<li>quantities</li>
<li>sequences</li>
</ul>

CC BY-SA 4.0
2023-12-10T21:56:45.050Z
2023-12-10T21:56:45.050Z
Martin Kealey
<p>I feel like someone has to make the argument from the other end, so I guess that person is me.</p>
<h1>CPUs typically support both</h1>
<p>In the vast majority of cases, CPUs don't care. It's just as easy for them to do signed 8-bit arithmetic as unsigned 8-bit arithmetic.</p>
<p>However, there are some asymmetries. The SSE2 <code>pcmpgtb</code> instruction, for example, performs a 16-way parallel greater-than test on <em>signed</em> 8-bit values. Unlike ARM Neon, there is no equivalent instruction for <em>unsigned</em> values.</p>
<p>Programmers can, of course, do various shenanigans if less-than is the only operation you need; just subtract 128 from each value prior to performing the test, for example.</p>
<p>Nonetheless, if optimisations such as automatic vectorisation are desired goals of a language, not allowing access to the &quot;fast&quot; version would seem to be a bad idea.</p>

CC BY-SA 4.0
2023-12-11T11:54:57.390Z
2023-12-11T11:54:57.390Z
Pseudonym
CC BY-SA 4.0
2024-01-25T14:11:31.430Z
If you're considering vector operations, then rather than add/subtract, what you want is `XOR`, since you can flip the sign bit of each operand within a word in a single operation. e.g. if you have 4 16-bit values in each 64-bit word, you can use `for (size_t i=0;i<n;++i) word[i] ^= 0x8000800080008000;` to ensure that the subsequent signed vectored comparison gives the correct answer
Martin Kealey
<p>The question &quot;should bytes be signed?&quot; isn't really a meaningful language design question. Generally speaking, we don't start by deciding what <em>names</em> we're going to have and then figure out what those names should refer to. Instead, we start by deciding what things we're going to have, and then we figure out what those things should be named.</p>
<p>The meaningful questions are:</p>
<ol>
<li>Should your language offer an unsigned 8-bit integer type?</li>
<li>Should your language offer a signed 8-bit integer type?</li>
<li>What should the above types be called? In particular, should one of them be called &quot;byte&quot;?</li>
</ol>
<p>The answers to questions 1 and 2 depend on what your goals are. At one extreme, Python 3 offers <em>no</em> fixed-width integer types; the only integer type you get is <code>int</code>, which theoretically can hold integers of any size whatsoever. That's because Python has a focus on ease of use, and for most purposes, an arbitrary-sized integer type is very easy to use. Near another extreme, the LLVM language offers integer types of <em>every</em> width from 1-bit up to 8388608-bit. That's because LLVM has a focus on being able to produce efficient machine code, and all bit widths are potentially useful for efficiency.</p>
<p>I will mention that by convention, the range of numbers from 0 to 255 seems to be used much more often than the range from -128 to 127, so if you're going to offer only one type or the other, you should probably offer the unsigned 8-bit integer type.</p>
<p>Once you've decided which types to offer, the next question is what to call them. Rust takes a very simple and sensible approach to naming integer types: an unsigned 8-bit is called <code>u8</code>, a signed 8-bit is called <code>i8</code>, an unsigned 16-bit is called <code>u16</code>, a signed 16-bit is called <code>i16</code>, and so on. It's hard to find any fault with this system!</p>
<p>In common usage, the word &quot;byte&quot; generally means &quot;nonnegative integer less than 256,&quot; so if you have an unsigned 8-bit integer type, naming it &quot;byte&quot; would certainly make sense. If you have a signed 8-bit integer type, naming it &quot;byte&quot; would be confusing; something like &quot;int8&quot; would be a much better name.</p>
<p><strong>TL;DR</strong>: Don't name <em>anything</em> a &quot;byte&quot; until after you've already decided what integer types you're going to have. It's more useful to have an unsigned 8-bit integer type than a signed 8-bit integer type. After you've decided on your integer types, either name the unsigned 8-bit integer type &quot;byte,&quot; or don't name any of them &quot;byte.&quot;</p>

CC BY-SA 4.0
2024-02-09T19:51:14.763Z
2024-02-09T19:51:14.763Z
Sophie Swett
CC BY-SA 4.0
2024-02-10T09:53:35.050Z
Tyr has both types for quite some time. Could you cite some statistic on why one would use signed over unsigned?
feldentm
CC BY-SA 4.0
2024-02-10T14:41:22.117Z
@feldentm I don't have any such statistics.
Sophie Swett
CC BY-SA 4.0
2024-02-12T10:24:05.360Z
It’s weird that you mention Python as a "counterexample" since it *does* have `bytes` which are immutable arrays of fixed size values. There just isn’t any specific type for an individual byte (which are commonly represented by a single-element `bytes` or an `int` in the range 0-255).
MisterMiyagi
CC BY-SA 4.0
2024-02-12T12:05:26.950Z
@MisterMiyagi Well, I wasn't really intending to give Python as a counterexample of anything. I was giving it as an example of a language that offers neither a type that means "an unsigned 8-bit integer" nor a type that means "a signed 8-bit integer."
Sophie Swett
<p>I would suggest having four integerish types of each size except the largest, specifically &quot;signed number&quot;, &quot;signed ring&quot;, &quot;unsigned number&quot;, and &quot;unsigned ring&quot;.  The largest size should have all but &quot;unsigned number&quot;.  Specifying things this way will ensure that for any computation performed between two integer &quot;number&quot; types, there will always be a type that can hold the values of both operands.  Since &quot;byte&quot; wouldn't be the largest type, it should be available in all four forms.</p>
<p>The difference between number types and ring types would be that number types would be expected to behave like natural integers for all non-erroneous computations, while n-bit ring types would have specified &quot;wrap-around&quot; behavior similar to a ring mod 2ⁿ.  An operation between two number types promote both operands to to a signed type large enough to handle both operands, an operation between a number and a ring would convert the number to the ring type, and operations on ring types would always yield a result of the same ring type (or an error if an operation on incompatible ring types would prevent that).</p>
<p>Although treating computations involving unsigned &quot;number&quot; types as though both were promoted to a larger signed type may not be the cheapest way of handling computations involving such types, it would be less likely than other approaches to yield results that were astonishing and inconsistent with programmer intention, especially if programmers use ring types when wrapping is intended.</p>

CC BY-SA 4.0
2023-12-11T08:16:09.817Z
2023-12-11T08:16:09.817Z
supercat
CC BY-SA 4.0
2023-12-11T15:57:28.720Z
Can you explain how to type literals in your proposal? I will not follow it in Tyr as I do not share your view on numbers, but I would be interested in understanding it. It could be interesting if I'd be tasked to work on a more math-oriented language.
feldentm
CC BY-SA 4.0
2023-12-11T16:06:16.693Z
@feldentm: Storing an integer literal or other integer "number" and a ring type would implicitly convert the number to that type.  Likewise using a binary operator with a number and a ring type.  The only times I could see any need to care about the type of an integer literal would be in cases where it was an operand to a binary operator which could use a type smaller than the largest type, but whose result was used as the left-hand operand of a right-shift, division, or remainder operator, or where the type might affect operator overloads selection, and I think casting in such cases...
supercat
CC BY-SA 4.0
2023-12-11T16:06:49.840Z
...would seem like a reasonable approach.
supercat
CC BY-SA 4.0
2023-12-11T18:31:01.503Z
@feldentm: Suppose `foo` and `bar` are of an unsigned type representing the ring of integers that are congruent mod 65536.  Saying `foo = bar+123;` when `bar` is congruent [mod 65536] to 65530 would make `bar` congruent [mod 65536] to (65530+123), which would in turn be congruent [mod 65536] to 117.  Because 123 and 65,659 are congruent mod 65536, saying `foo = bar+65659;` would have the same effect.  Whether the literal was an 8-bit, 16-bit, 32-bit, or 64-bit value would be irrelevant to the result, which would always be truncated to 16 bits regardless.
supercat
CC BY-SA 4.0
2023-12-12T17:02:01.050Z
So you would add an omnipotent literal type? I'm struggling a bit with what the CT evaluation of something like `2 << 64 + 1` would be. But maybe I make some hidden assumptions here.
feldentm
CC BY-SA 4.0
2023-12-12T17:23:59.133Z
@feldentm: I would add a semi-omnipotent literal type (able to represent any integer +/- the largest ring modulus, and any floating-point value representable within the language).  I'd also include a compile-time string type, so as to allow e.g. "string literals" to include content based on things like build date.  I'd evaluate expression types outside-in, except that casts may be required on the left-side operands to the integer-division and integer-right-shift operators in situations where use of longer types might support more use cases, but have a higher run-time cost.  E.g. given...
supercat
CC BY-SA 4.0
2023-12-12T17:25:31.957Z
...`int64a = int32a*int32b;`, I'd specify that the multiply would yield a 64-bit result, but given `int32a = int32b*int3c / int32d;`, I'd require a cast on the result of the multiply, since using 32 bits may be faster than using 64 bits in cases where it would work, but a compiler would have no way of knowing whether it would handle all relevant cases.
supercat
CC BY-SA 4.0
2023-12-27T23:40:49.890Z
I like the idea that numbers behave as near as possible to true integers, but I'm not sure that basing this decision solely on the type of operand is necessarily the best way to achieve it. An alternative to consider would be deciding on modulo vs natural depending on what is being assigned to (or the formal parameter type when calling a function, or the return type when returning from a function).
Martin Kealey
CC BY-SA 4.0
2023-12-27T23:42:16.537Z
A third option could be to have different operators; for example, have `+` and `-` apply natural integer semantics, while `%+` and `%-` apply modulo $2^n$ semantics.
Martin Kealey
CC BY-SA 4.0
2023-12-27T23:47:14.060Z
An adaptation of this idea is to define types using range declarations (with or without modulus). So could see things like: `type ubyte = 0 ... 255` or `type sbyte = -128 ... 127` or `type ssize_t = -1 ... ~(1ULR)` (like C, a suffix of `U` or `S` denotes unsigned or signed, `L` denotes long(er); in addition `R` denotes Ring)
Martin Kealey
<p>A file is an ordered set of bytes.  A string is an ordered set of characters.  A network packet is an ordered set of octets.</p>
<p>We wouldn't think of the file as being composed of signed bytes, nor the string composed of signed characters, nor the network octets being signed.  These argue for unsigned bytes.</p>
<p>I'd like to hear analogous arguments in favor of signed bytes, but I can't think of any.</p>

CC BY-SA 4.0
2023-12-09T18:25:00.170Z
2023-12-09T18:25:00.170Z
Erik Eidt
CC BY-SA 4.0
2023-12-09T19:06:43.157Z
When we think of a file as a sequence of bytes, we don't even assume that those bytes are supposed to be interpreted as numbers ─ often they are supposed to represent characters, for example, or some other kind of binary data. If `01100001` represents the character `'a'` then it doesn't matter to us what it would mean if it represented a number instead. So I don't think this really explains why an 8-bit number type should be unsigned.
kaya3
CC BY-SA 4.0
2023-12-10T12:52:14.720Z
@kaya3 Isn't that exactly what this answer is arguing for? A `byte` is not a number, they do not have signs. For `u8` and `i8` one should provide both options.
Bergi
CC BY-SA 4.0
2023-12-10T13:04:16.430Z
If a bit pattern isn't meant to represent a number then yes, it cannot be signed; but also it cannot be unsigned, because that is also something which only makes sense if the type is numeric. You could equally argue that strings, lists and hashmaps cannot be signed (because they aren't numbers) so therefore they are unsigned; but nobody goes around talking about unsigned strings or unsigned lists, and it would be nonsense to ask if hashmaps should be signed or unsigned. So I think the question must be understood as asking about numeric byte types.
kaya3
CC BY-SA 4.0
2023-12-10T15:37:07.697Z
Perhaps you're arguing for an opaque byte type, neither signed nor unsigned.  One can do with bytes whatever is supported, i.e. convert to string, write to file, send as packet.  But not inspect their individual values without make a choice of what numeric representation you want to see.  Perhaps that choice should be left to the code to decide on demand and at use rather than pre-determined by numeric properties of a type.
Erik Eidt
CC BY-SA 4.0
2023-12-26T04:31:56.793Z
Sadly I've seen large projects (*cough* _Exim_ *cough*) where strings are now "unsigned" because the author couldn't identify the few cases where implicitly signed char was causing a problem, so instead they did a huge bulk update to make everything explicitly `unsigned char`, and then added a bunch of conversions for when library calls expected bare `char`. Those conversions were blind casts, meaning they weakened the typechecking for future code.
Martin Kealey
<p><em>Frame Challenge</em>: <strong>Bytes should not be integers</strong>.</p>
<p>Bytes are bytes, a fixed-size sequence of bits, typically 8 on modern computers.</p>
<p>Should an array of bits be signed or unsigned? Neither, the question is nonsensical: an array cannot be signed or unsigned in the first place.</p>
<p>There's a long-standing tradition in programming languages to conflate integers and array of bits, which can be seen in that bit-wise operations are typically defined on integers. I argue that this &quot;blending&quot; of the two roles is a mistake: a violation of the Single Responsibility Principle.</p>
<p>Yes, it's all registers at the end... should a pointer be signed or unsigned? Surely nowadays the question seems nonsensical, even though in B pointers and integers were the same type, and in CPU they are manipulated through the same registers.</p>
<p>Just like pointers are different from integers, I hereby argue that array of bits should be different from integers, and bit-wise functions are only sensible on array of bits.</p>
<p>The conversion between integer and array of bits (of the same number of bits) should exist -- just like that between floating point or decimal an array of bits -- and it should ideally boil down to a no-op, but from a <em>type</em> system, the two should be separate.</p>

CC BY-SA 4.0
2023-12-10T17:19:34.330Z
2023-12-10T17:19:34.330Z
Matthieu M.
CC BY-SA 4.0
2023-12-10T17:29:29.343Z
This is quite a hot take. There are plenty of reasons to do arithmetic with 8-bit numbers ─ e.g. certain PRNGs and cryptographic algorithms, or emulating 8-bit processors ─ so either you accept that 8-bit numbers should exist (in which case this answer only quibbles about what they should be called, when the question is whether they should be signed or unsigned), or you're arguing that that arithmetic should not be supported directly.
kaya3
CC BY-SA 4.0
2023-12-10T19:06:51.690Z
@kaya3: I am must admit that `byte` immediately evokes memory view in my mind, and the mention of `std::byte` definitely reinforced it. Re-reading the question, I suppose it could be understood as asking about 8-bits arithmetic... but if so I would argue it is quite unclear as in my first reading it completely eluded me. If it wishes to mention 8-bits arithmetic, it should really mention so.
Matthieu M.
CC BY-SA 4.0
2023-12-10T19:41:27.487Z
TBH, my question would be what the literal subtype for byte would be or how to manipulate bytes in the sense of `b ^ 0x3f`. The literal must have some compatible type. Voted nonetheless, as I think the answer is a valuable and overall valid contribution.
feldentm
CC BY-SA 4.0
2023-12-11T08:17:52.063Z
@feldentm: I would argue a byte literal, or possibly a bit array literal? If your language features type inference, you may have "undecided" types for the literals. For example, Rust error messages regularly display `{integer}` for an integral literal whose type could not be decided: the actual type is -- when it works -- decided based on context.
Matthieu M.
CC BY-SA 4.0
2023-12-11T12:00:59.900Z
RE: "Should a point be signed or unsigned?", this is a feature of the hardware, not of the programming language. This is significant for example on the PDP-10, where negative pointers are possible only in supervisor mode. Also there's some funk going on with AMD64 as well which could be argued has a signed pointer datatype, even involving sign-extension.
Omar and Lorraine
CC BY-SA 4.0
2023-12-11T15:02:25.510Z
@kaya3: "There are plenty of reasons to do arithmetic with 8-bit numbers". Indeed. That's why the language should also have both a signed and an unsigned 8-bit integer type, distinct from `byte`.
Tomeamis
CC BY-SA 4.0
2023-12-11T16:15:29.673Z
IMHO, languages should have types of a variety of sizes that behave like integer numbers, and types in a variety of sizes that behave like wrapping algebraic rings.  Algebraic rings should support the same kinds of operations as integers, but should generally not promote nor be convertible to numbers without casts except when the type of an expression is "self-evident".
supercat
CC BY-SA 4.0
2023-12-12T09:39:29.743Z
A possible naming scheme could be uint8, int8 and bit8 for unsigned, signed and bit-array all with a width of 8 bit
Falco
CC BY-SA 4.0
2023-12-12T10:23:55.347Z
@kaya3 notice how you said "8-bit numbers" and not "bytes"?  Because you already accepted that they are semantically different. Yes, it makes sense to operate on 8 bit numbers. Who then happen to also be bytes. Just like I can drive a car but not drive an object. 8 bit numbers are a subtype of byte, but thats unidirectional
Hobbamok
CC BY-SA 4.0
2023-12-12T18:00:06.363Z
@Hobbamok I said "8-bit numbers" instead of "bytes" because the context of the discussion makes clear that if I had said "bytes", the person I was talking to could have quibbled over whether I was using the word "bytes" correctly, and I thought that would be a useless discussion to have. Since you want to have it anyway, I think there is nothing wrong with using the word "bytes" as the question does to refer to a numeric type, and I think it *is* clear that the question is about numeric types and not opaque bit patterns. (It's tagged [tag:integer] and it asks about signedness.)
kaya3
CC BY-SA 4.0
2023-12-13T03:36:10.100Z
C/C++'s conflating of "8 bit integers" and "raw collection 8 bits" causes problems.  Note that once you drop bytes as integers, you can have 8, 16, 32, 64, 128 (etc) first-class fixed size bit types; heck, bits^n type can be a fixed size 2^n type, and be limited by hardware (the non-power-of-two types are far less powerful in a low level language, as hardware really likes powers of 2; if your language is low level you can skip them, if higher include them)
Yakk - Adam Nevraumont
CC BY-SA 4.0
2023-12-13T08:13:48.727Z
@kaya3: I'll disagree that it's _clear_ that the question is about integers when it references `std::byte` which is purposefully NOT an integer, and meant specifically to view raw bytes (files, memory, etc...). For example, there's no arithmetic operations on `std::byte`.
Matthieu M.
CC BY-SA 4.0
2023-12-17T16:47:16.943Z
"should a pointer be signed or unsigned? Surely nowadays the question seems nonsensical, even though in B pointers and integers were the same type, and in CPU they are manipulated through the same registers." In *some* CPUs pointers and integers are stored/manipulated in the same registers. But in others, they aren't (at least normally). The 68K, for example had A0 through A7 for pointers and R0 through R7 for integers.
Jerry Coffin
CC BY-SA 4.0
2023-12-17T16:49:03.473Z
For old fogies, CDC mainframes had A and B registers for pointers, and X registers for data. In this case you kind of had to use the A and B registers for pointers. Each X register had matching A and B registers. You loaded an X register by writing a pointer to its corresponding A register, and did a store by writing a pointer to its B register.
Jerry Coffin
# Should bytes be signed?
Posted by: username_0 on 2023-12-09
I’m trying to decide whether my language should provide signed or unsigned bytes, but I’m struggling to find a good reason to choose either side.

Popular languages vary in their decision. For example, Java uses signed bytes, although they have unsigned shorts. C++ has unsigned bytes ([std::byte](https://en.cppreference.com/w/cpp/types/byte)), but I've seen a lot of code using signed chars as bytes.

Is there any consensus view on whether bytes ought to be signed or unsigned by default? Answers should ideally provide citations to back up their claims.
**Tags:** language-design standard-library integers
> **Comments:**
> **username_0**: @RydwolfPrograms the question is meant the other way round. It's `type byte := i8` or `type byte := u8`? The language has both types already irrespective of the byte type.  
> **username_1**: **Comments have been [moved to chat](https://chat.stackexchange.com/rooms/150275/discussion-on-question-by-feldentm-should-bytes-be-signed); please do not continue the discussion here.** Before posting a comment below this one, please review the [purposes of comments](/help/privileges/comment). Comments that do not request clarification or suggest improvements usually belong as an [answer](/help/how-to-answer), on [meta], or in [chat]. Comments continuing discussion may be removed.  
> **username_2**: C++'s primary byte types isn't `std::byte`, it's `char` which is implementation defined as to whether it is signed or unsigned; and `std::byte` isn't really unsigned or signed.  
> **username_3**: (A comment as I'm not yet able to post answers to this Q) You should maybe have a look at more recent languages which may have learned a bit from the mistakes of the past. For example, Rust has a fairly symmetrical set of explicitly signed and unsigned integer types for the various bit lengths. There are u8 and i8 (unsigned and signed 8-bit integers) as well as u16/i16, u32/i32, u64/i64 and u128/i128.  
> **username_0**: @Hans-MartinMosner The language has signed and unsigned for arbitrary fixed sizes. The question is specifically about the concept of bytes. While I thought that the solution would be an alias like `byte := i8` or `byte := u8` in your terminology, I guess Matthieu convinced me that this might not be a good idea.  
---
## 9 answers
### Answer by username_17 on 2023-12-10. Score: 48 | Accepted answer
*Frame Challenge*: **Bytes should not be integers**.

Bytes are bytes, a fixed-size sequence of bits, typically 8 on modern computers.

Should an array of bits be signed or unsigned? Neither, the question is nonsensical: an array cannot be signed or unsigned in the first place.

There's a long-standing tradition in programming languages to conflate integers and array of bits, which can be seen in that bit-wise operations are typically defined on integers. I argue that this "blending" of the two roles is a mistake: a violation of the Single Responsibility Principle.

Yes, it's all registers at the end... should a pointer be signed or unsigned? Surely nowadays the question seems nonsensical, even though in B pointers and integers were the same type, and in CPU they are manipulated through the same registers.

Just like pointers are different from integers, I hereby argue that array of bits should be different from integers, and bit-wise functions are only sensible on array of bits.

The conversion between integer and array of bits (of the same number of bits) should exist -- just like that between floating point or decimal an array of bits -- and it should ideally boil down to a no-op, but from a *type* system, the two should be separate.
> **Comments:**
> **username_4** (2023-12-10): This is quite a hot take. There are plenty of reasons to do arithmetic with 8-bit numbers ─ e.g. certain PRNGs and cryptographic algorithms, or emulating 8-bit processors ─ so either you accept that 8-bit numbers should exist (in which case this answer only quibbles about what they should be called, when the question is whether they should be signed or unsigned), or you're arguing that that arithmetic should not be supported directly.  
> **username_17** (2023-12-10): @kaya3: I am must admit that `byte` immediately evokes memory view in my mind, and the mention of `std::byte` definitely reinforced it. Re-reading the question, I suppose it could be understood as asking about 8-bits arithmetic... but if so I would argue it is quite unclear as in my first reading it completely eluded me. If it wishes to mention 8-bits arithmetic, it should really mention so.  
> **username_0** (2023-12-10): TBH, my question would be what the literal subtype for byte would be or how to manipulate bytes in the sense of `b ^ 0x3f`. The literal must have some compatible type. Voted nonetheless, as I think the answer is a valuable and overall valid contribution.  
> **username_17** (2023-12-11): @feldentm: I would argue a byte literal, or possibly a bit array literal? If your language features type inference, you may have "undecided" types for the literals. For example, Rust error messages regularly display `{integer}` for an integral literal whose type could not be decided: the actual type is -- when it works -- decided based on context.  
> **username_18** (2023-12-11): RE: "Should a point be signed or unsigned?", this is a feature of the hardware, not of the programming language. This is significant for example on the PDP-10, where negative pointers are possible only in supervisor mode. Also there's some funk going on with AMD64 as well which could be argued has a signed pointer datatype, even involving sign-extension.  
> **username_19** (2023-12-11): @kaya3: "There are plenty of reasons to do arithmetic with 8-bit numbers". Indeed. That's why the language should also have both a signed and an unsigned 8-bit integer type, distinct from `byte`.  
> **username_8** (2023-12-11): IMHO, languages should have types of a variety of sizes that behave like integer numbers, and types in a variety of sizes that behave like wrapping algebraic rings.  Algebraic rings should support the same kinds of operations as integers, but should generally not promote nor be convertible to numbers without casts except when the type of an expression is "self-evident".  
> **username_20** (2023-12-12): A possible naming scheme could be uint8, int8 and bit8 for unsigned, signed and bit-array all with a width of 8 bit  
> **username_21** (2023-12-12): @kaya3 notice how you said "8-bit numbers" and not "bytes"?  Because you already accepted that they are semantically different. Yes, it makes sense to operate on 8 bit numbers. Who then happen to also be bytes. Just like I can drive a car but not drive an object. 8 bit numbers are a subtype of byte, but thats unidirectional  
> **username_4** (2023-12-12): @Hobbamok I said "8-bit numbers" instead of "bytes" because the context of the discussion makes clear that if I had said "bytes", the person I was talking to could have quibbled over whether I was using the word "bytes" correctly, and I thought that would be a useless discussion to have. Since you want to have it anyway, I think there is nothing wrong with using the word "bytes" as the question does to refer to a numeric type, and I think it *is* clear that the question is about numeric types and not opaque bit patterns. (It's tagged [tag:integer] and it asks about signedness.)  
> **username_22** (2023-12-13): C/C++'s conflating of "8 bit integers" and "raw collection 8 bits" causes problems.  Note that once you drop bytes as integers, you can have 8, 16, 32, 64, 128 (etc) first-class fixed size bit types; heck, bits^n type can be a fixed size 2^n type, and be limited by hardware (the non-power-of-two types are far less powerful in a low level language, as hardware really likes powers of 2; if your language is low level you can skip them, if higher include them)  
> **username_17** (2023-12-13): @kaya3: I'll disagree that it's _clear_ that the question is about integers when it references `std::byte` which is purposefully NOT an integer, and meant specifically to view raw bytes (files, memory, etc...). For example, there's no arithmetic operations on `std::byte`.  
> **username_23** (2023-12-17): "should a pointer be signed or unsigned? Surely nowadays the question seems nonsensical, even though in B pointers and integers were the same type, and in CPU they are manipulated through the same registers." In *some* CPUs pointers and integers are stored/manipulated in the same registers. But in others, they aren't (at least normally). The 68K, for example had A0 through A7 for pointers and R0 through R7 for integers.  
> **username_23** (2023-12-17): For old fogies, CDC mainframes had A and B registers for pointers, and X registers for data. In this case you kind of had to use the A and B registers for pointers. Each X register had matching A and B registers. You loaded an X register by writing a pointer to its corresponding A register, and did a store by writing a pointer to its B register.  
---
### Answer by username_5 on 2023-12-09. Score: 20
## Bytes should be unsigned

There are a few reasons most languages have unsigned bytes by default:

* A more useful range. Bytes can only represent 256 different numbers so using them effectively is extra important. It is more common to want to represent a bigger number like 200 than -1, so the 0-255 is more practical than -128-127.

This isn't really an issue for bigger numbers that usually default to signed. Even an signed short can go up to more than 30,000 which is not something you would typically count to.

* Bytes are most often used as a sequence, rather than as an actual number. You rarely use a type like a byte to store numbers intended as numbers, more often they represent bits of binary data, characters etc.

The "numeric properties" only get in the way of accessing the raw data. Adding a special meaning to the upper half of the byte space would just add extra complexity to any serialization, parsing, string manipulation, etc. code that may want to operate on byte sequences.
> **Comments:**
> **username_6** (2023-12-11): I'd like to suggest one additional reason to keep them unsigned (this isn't enough to warrant a separate answer): it makes the hex representation consistent. For example, Java's bytes are signed, which means you can't do `byte b = 0xff` (that's out of range). But if you have `byte b = -1` and then view it as hex, that becomes `0xff`. This is true of any signed type, but it's very common to view bytes as hex, so it's especially noticeable there. (This is basically a special scenario within your point that "bytes are most often used as a sequence, rather than as an actual number.")  
> **username_7** (2023-12-12): I think you meant to say "Even a **signed** short can go up to more than 30,000", since you're making the point that even 32767 is often enough, not needing UINT16_MAX 65535.  
> **username_8** (2023-12-12): @yshavit: I would think hex notation could be improved by saying that 0y123 would represent a number whose bits are all set except for the last 12 bits which would represent the bit pattern 0x123.  The number below zero would in hex thus be represented interchangeably as 0yFF, 0yFFFF, 0yFFFFFFFF, etc., without needing to treat the number of leading F's as significant.  
> **username_6** (2023-12-13): @supercat I'm not quite sure what you mean, sorry. Are you suggesting an alternate form of representing hex digits, that basically represents just the negative-int space of twos complement? If so, I think the thing is that very often when you're representing hex, you're actually thinking of it as a bitset — not as an int, signed or otherwise (html color codes are one obvious exception). But having the underlying data type be an unsigned int maps a bit better to that bitset perspective than a signed int would.  
> **username_8** (2023-12-13): @yshavit: The idea would be that `0y` followed by digits would yield the signed value that starts with an infinite number of `F`'s followed by the specified value.  Especially useful when masking the bottom bits of a number, and one wants to leave the upper bits alone.  
> **username_4** (2024-01-18): I realise I'm late to this, but that would be confusing when `0y00` and `0y0000` don't mean the same thing, given that `0x00` and `0x0000` do.  
> **username_9** (2024-01-24): Bytes are also commonly used to store enumeration values, since it's rare that an enumeration would require more than 256 cases. For enumerations, a numeric value at all is usually no more than an implementation detail; but caring about the ability to make those values negative is even less useful.  
---
### Answer by username_4 on 2023-12-10. Score: 10
Signed bytes make bit-packing difficult, because when a signed byte is converted to a wider integer type, sign extension means that the wider integer might use more than just the low 8 bits. Consider packing e.g. an IPv4 address into a 32-bit integer: the signed version needs & 0xFF to unset the sign-extended bits.
fn pack_ipv4(a: u8, b: u8, c: u8, d: u8) -> u32 {
    (a as u32) << 24
    | (b as u32) << 16
    | (c as u32) << 8
    | (d as u32)
}

fn pack_ipv4_signed(a: i8, b: i8, c: i8, d: i8) -> u32 {
    (a as u32 & 0xFF) << 24
    | (b as u32 & 0xFF) << 16
    | (c as u32 & 0xFF) << 8
    | (d as u32 & 0xFF)
}

The main reason to have bytes at all is their compact representation. So it should be as easy as possible to convert between the packed and unpacked representation. Unsigned bytes are better for this, since conversions don't involve sign extension.
> **Comments:**
> **username_5** (2023-12-10): Making the signed version output a `i32` might be a more fair comparison, since you wouldn't need to set the sign for the leftmost byte  
> **username_4** (2023-12-10): @mousetail Well, you wouldn't need the `& 0xFF` for the left-most byte either way, because the shift will drop all of the sign-extended bits. But personally, either way, I would write it this way because it's more obviously correct. The alternative would be to omit the one `& 0xFF` and write a comment explaining why it's not needed.  
---
### Answer by username_11 on 2023-12-10. Score: 8
This still boils down to "what do you intend to do with them?"

As others have noted, sign extension is a nuisance when deserializing a wider integer, which suggests that unsigned is more useful.

Conversely, if `x` is signed, `x<0` will tell you whether its top bit is set without having to know how wide it is.

I'm not convinced by arguments over the semantics imposed on the bytes; everything is a bunch of bits if you look closely enough, and arithmetic operations can be useful low-level operations on things that are not semantically "numbers".

Even the idea of "number" is a conflation of several things, which ideally a type system would keep distinct:

* identifiers
* ordinals
* quantities
* sequences
---
### Answer by username_10 on 2023-12-09. Score: 4
If they're signed, then you might have to worry about sign representation issues.

If they're unsigned by default, then their set of values will always have a consistent mapping to their bit pattern, regardless the sign representation.

But this almost doesn't even matter, because Two's Complement is the prevailing sign representation, and others are seldom, if ever, seen, especially in 2023.
---
### Answer by username_12 on 2023-12-11. Score: 3
I feel like someone has to make the argument from the other end, so I guess that person is me.

# CPUs typically support both

In the vast majority of cases, CPUs don't care. It's just as easy for them to do signed 8-bit arithmetic as unsigned 8-bit arithmetic.

However, there are some asymmetries. The SSE2 `pcmpgtb` instruction, for example, performs a 16-way parallel greater-than test on *signed* 8-bit values. Unlike ARM Neon, there is no equivalent instruction for *unsigned* values.

Programmers can, of course, do various shenanigans if less-than is the only operation you need; just subtract 128 from each value prior to performing the test, for example.

Nonetheless, if optimisations such as automatic vectorisation are desired goals of a language, not allowing access to the "fast" version would seem to be a bad idea.
> **Comments:**
> **username_11** (2024-01-25): If you're considering vector operations, then rather than add/subtract, what you want is `XOR`, since you can flip the sign bit of each operand within a word in a single operation. e.g. if you have 4 16-bit values in each 64-bit word, you can use `for (size_t i=0;i<n;++i) word[i] ^= 0x8000800080008000;` to ensure that the subsequent signed vectored comparison gives the correct answer  
---
### Answer by username_13 on 2024-02-09. Score: 3
The question "should bytes be signed?" isn't really a meaningful language design question. Generally speaking, we don't start by deciding what *names* we're going to have and then figure out what those names should refer to. Instead, we start by deciding what things we're going to have, and then we figure out what those things should be named.

The meaningful questions are:

1. Should your language offer an unsigned 8-bit integer type?
2. Should your language offer a signed 8-bit integer type?
3. What should the above types be called? In particular, should one of them be called "byte"?

The answers to questions 1 and 2 depend on what your goals are. At one extreme, Python 3 offers *no* fixed-width integer types; the only integer type you get is `int`, which theoretically can hold integers of any size whatsoever. That's because Python has a focus on ease of use, and for most purposes, an arbitrary-sized integer type is very easy to use. Near another extreme, the LLVM language offers integer types of *every* width from 1-bit up to 8388608-bit. That's because LLVM has a focus on being able to produce efficient machine code, and all bit widths are potentially useful for efficiency.

I will mention that by convention, the range of numbers from 0 to 255 seems to be used much more often than the range from -128 to 127, so if you're going to offer only one type or the other, you should probably offer the unsigned 8-bit integer type.

Once you've decided which types to offer, the next question is what to call them. Rust takes a very simple and sensible approach to naming integer types: an unsigned 8-bit is called `u8`, a signed 8-bit is called `i8`, an unsigned 16-bit is called `u16`, a signed 16-bit is called `i16`, and so on. It's hard to find any fault with this system!

In common usage, the word "byte" generally means "nonnegative integer less than 256," so if you have an unsigned 8-bit integer type, naming it "byte" would certainly make sense. If you have a signed 8-bit integer type, naming it "byte" would be confusing; something like "int8" would be a much better name.

**TL;DR**: Don't name *anything* a "byte" until after you've already decided what integer types you're going to have. It's more useful to have an unsigned 8-bit integer type than a signed 8-bit integer type. After you've decided on your integer types, either name the unsigned 8-bit integer type "byte," or don't name any of them "byte."
> **Comments:**
> **username_0** (2024-02-10): Tyr has both types for quite some time. Could you cite some statistic on why one would use signed over unsigned?  
> **username_13** (2024-02-10): @feldentm I don't have any such statistics.  
> **username_14** (2024-02-12): It’s weird that you mention Python as a "counterexample" since it *does* have `bytes` which are immutable arrays of fixed size values. There just isn’t any specific type for an individual byte (which are commonly represented by a single-element `bytes` or an `int` in the range 0-255).  
> **username_13** (2024-02-12): @MisterMiyagi Well, I wasn't really intending to give Python as a counterexample of anything. I was giving it as an example of a language that offers neither a type that means "an unsigned 8-bit integer" nor a type that means "a signed 8-bit integer."  
---
### Answer by username_8 on 2023-12-11. Score: 2
I would suggest having four integerish types of each size except the largest, specifically "signed number", "signed ring", "unsigned number", and "unsigned ring". The largest size should have all but "unsigned number". Specifying things this way will ensure that for any computation performed between two integer "number" types, there will always be a type that can hold the values of both operands. Since "byte" wouldn't be the largest type, it should be available in all four forms.

The difference between number types and ring types would be that number types would be expected to behave like natural integers for all non-erroneous computations, while n-bit ring types would have specified "wrap-around" behavior similar to a ring mod 2ⁿ. An operation between two number types promote both operands to to a signed type large enough to handle both operands, an operation between a number and a ring would convert the number to the ring type, and operations on ring types would always yield a result of the same ring type (or an error if an operation on incompatible ring types would prevent that).

Although treating computations involving unsigned "number" types as though both were promoted to a larger signed type may not be the cheapest way of handling computations involving such types, it would be less likely than other approaches to yield results that were astonishing and inconsistent with programmer intention, especially if programmers use ring types when wrapping is intended.
> **Comments:**
> **username_0** (2023-12-11): Can you explain how to type literals in your proposal? I will not follow it in Tyr as I do not share your view on numbers, but I would be interested in understanding it. It could be interesting if I'd be tasked to work on a more math-oriented language.  
> **username_8** (2023-12-11): @feldentm: Storing an integer literal or other integer "number" and a ring type would implicitly convert the number to that type.  Likewise using a binary operator with a number and a ring type.  The only times I could see any need to care about the type of an integer literal would be in cases where it was an operand to a binary operator which could use a type smaller than the largest type, but whose result was used as the left-hand operand of a right-shift, division, or remainder operator, or where the type might affect operator overloads selection, and I think casting in such cases...  
> **username_8** (2023-12-11): ...would seem like a reasonable approach.  
> **username_8** (2023-12-11): @feldentm: Suppose `foo` and `bar` are of an unsigned type representing the ring of integers that are congruent mod 65536.  Saying `foo = bar+123;` when `bar` is congruent [mod 65536] to 65530 would make `bar` congruent [mod 65536] to (65530+123), which would in turn be congruent [mod 65536] to 117.  Because 123 and 65,659 are congruent mod 65536, saying `foo = bar+65659;` would have the same effect.  Whether the literal was an 8-bit, 16-bit, 32-bit, or 64-bit value would be irrelevant to the result, which would always be truncated to 16 bits regardless.  
> **username_0** (2023-12-12): So you would add an omnipotent literal type? I'm struggling a bit with what the CT evaluation of something like `2 << 64 + 1` would be. But maybe I make some hidden assumptions here.  
> **username_8** (2023-12-12): @feldentm: I would add a semi-omnipotent literal type (able to represent any integer +/- the largest ring modulus, and any floating-point value representable within the language).  I'd also include a compile-time string type, so as to allow e.g. "string literals" to include content based on things like build date.  I'd evaluate expression types outside-in, except that casts may be required on the left-side operands to the integer-division and integer-right-shift operators in situations where use of longer types might support more use cases, but have a higher run-time cost.  E.g. given...  
> **username_8** (2023-12-12): ...`int64a = int32a*int32b;`, I'd specify that the multiply would yield a 64-bit result, but given `int32a = int32b*int3c / int32d;`, I'd require a cast on the result of the multiply, since using 32 bits may be faster than using 64 bits in cases where it would work, but a compiler would have no way of knowing whether it would handle all relevant cases.  
> **username_11** (2023-12-27): I like the idea that numbers behave as near as possible to true integers, but I'm not sure that basing this decision solely on the type of operand is necessarily the best way to achieve it. An alternative to consider would be deciding on modulo vs natural depending on what is being assigned to (or the formal parameter type when calling a function, or the return type when returning from a function).  
> **username_11** (2023-12-27): A third option could be to have different operators; for example, have `+` and `-` apply natural integer semantics, while `%+` and `%-` apply modulo $2^n$ semantics.  
> **username_11** (2023-12-27): An adaptation of this idea is to define types using range declarations (with or without modulus). So could see things like: `type ubyte = 0 ... 255` or `type sbyte = -128 ... 127` or `type ssize_t = -1 ... ~(1ULR)` (like C, a suffix of `U` or `S` denotes unsigned or signed, `L` denotes long(er); in addition `R` denotes Ring)  
---
### Answer by username_15 on 2023-12-09. Score: 2
A file is an ordered set of bytes.  A string is an ordered set of characters.  A network packet is an ordered set of octets.

We wouldn't think of the file as being composed of signed bytes, nor the string composed of signed characters, nor the network octets being signed.  These argue for unsigned bytes.

I'd like to hear analogous arguments in favor of signed bytes, but I can't think of any.
> **Comments:**
> **username_4** (2023-12-09): When we think of a file as a sequence of bytes, we don't even assume that those bytes are supposed to be interpreted as numbers ─ often they are supposed to represent characters, for example, or some other kind of binary data. If `01100001` represents the character `'a'` then it doesn't matter to us what it would mean if it represented a number instead. So I don't think this really explains why an 8-bit number type should be unsigned.  
> **username_16** (2023-12-10): @kaya3 Isn't that exactly what this answer is arguing for? A `byte` is not a number, they do not have signs. For `u8` and `i8` one should provide both options.  
> **username_4** (2023-12-10): If a bit pattern isn't meant to represent a number then yes, it cannot be signed; but also it cannot be unsigned, because that is also something which only makes sense if the type is numeric. You could equally argue that strings, lists and hashmaps cannot be signed (because they aren't numbers) so therefore they are unsigned; but nobody goes around talking about unsigned strings or unsigned lists, and it would be nonsense to ask if hashmaps should be signed or unsigned. So I think the question must be understood as asking about numeric byte types.  
> **username_15** (2023-12-10): Perhaps you're arguing for an opaque byte type, neither signed nor unsigned.  One can do with bytes whatever is supported, i.e. convert to string, write to file, send as packet.  But not inspect their individual values without make a choice of what numeric representation you want to see.  Perhaps that choice should be left to the code to decide on demand and at use rather than pre-determined by numeric properties of a type.  
> **username_11** (2023-12-26): Sadly I've seen large projects (*cough* _Exim_ *cough*) where strings are now "unsigned" because the author couldn't identify the few cases where implicitly signed char was causing a problem, so instead they did a huge bulk update to make everything explicitly `unsigned char`, and then added a bunch of conversions for when library calls expected bare `char`. Those conversions were blind casts, meaning they weakened the typechecking for future code.  
---
