C's Flexible Integer Sizes Were Not a Design Mistake

pikuma.com

61 points by ibobev 2 days ago


layer8 - 2 hours ago

> Language types such as char, int, short, and long do not come with a guarantee of how many bytes they occupy in memory.

Char is actually guaranteed by C to occupy exactly 1 byte in memory. It’s just that a byte can have more than eight bits in C. “Byte” is simply the smallest unit of memory addressable by a pointer.

Further down the article acknowledges that “C requires char to have at least 8 bits (CHAR_BIT >= 8), not exactly 8” and mentions the Honeywell 6000 as an example of a C implementation with 9 bits (and 36-bit ints).

Historically in computing, the size of a byte was hardware-dependent and not standardized. The Wikipedia article on “byte” cites Knuth’s 1968 TAOCP where byte denotes a unit which “contains an unspecified amount of information […] capable of holding at least 64 distinct values […] at most 100 distinct values. On a binary computer a byte must therefore be composed of six bits”.

adrian_b - 12 hours ago

I agree that the C flexible integer sizes were still necessary at the time of its creation, when some important computers still had word sizes that were not powers of two.

Nonetheless, I started to use C for programming only in 1990, when I got access to the Microsoft C and Borland Turbo C compilers.

At that time, 36 years ago, the C flexible integer sizes were already obsolete.

Since that time until now, while using C on a great variety of computers, from servers and workstations to the smallest microcontrollers, I have seen plenty of portability problems created by the existence of the flexible integer sizes.

The only programs that had no portability problems were those that never used the flexible integer sizes, but only integers with a definite size, e.g. 8-bit, 16-bit, 32-bit or 64-bit.

While sizeof solves the problems of memory allocation or copying, it does not help in preventing unexpected integer overflows, because even the size of "char" may be unknown, and even if the size of "char" is known, writing code with multiple paths that would check or prevent overflow for different integer sizes is very cumbersome.

Flexible integer sizes would work well only on the old computers, where integer overflow generated a hardware exception, so installing an overflow handler would have been sufficient to make the C code work correctly regardless of the size of the native integers.

InvisibleUp - 27 minutes ago

Where the flexible integer sizes break the most is when dealing with ABIs, which weren't really a concern before dynamic linking existed but are very much a concern today. We've also, for some reason, decided that the standard way of defining a library ABI is with a C header. That means that everyone has to worry about precisely defining integer sizes, as well as more esoteric types like size_t or intmax_t. Good writeup on all that here: https://thephd.dev/to-save-c-we-must-save-abi-fixing-c-funct...

habitue - 27 minutes ago

It was intentional, sure. It was an attempt to solve a particular kind of problem.

In hindsight though, it was a mistake.

Evidence: when the world moved to 64 bit, we didnt just let int mean 8 bytes on amd64. That's a clear acknowledgement that the design was not correct once we understood things better.

codedokode - an hour ago

I think it didn't work out well, because "int" being different size makes programming difficult. For example, a system must manage up to 100 000 records. Can I use int for record number? What if it is 16 bits? What if I need to send data between machines, how can I use "int" if it can be different size?

Probably someone noticed that it is inconvenient, and on 64-bit machine ints are still 32-bit and not 64.

The computers with 16-bit ints or 9-bit bytes are long gone, but the language still has to carry that legacy.

quelsolaar - 12 hours ago

Good article.

C would probably not have survived unless it had this flexibility.

But its not justa historical thing. Today there are modern platforms like DSPs that have 32bit sized char, because that is the smallest addressable type. These platforms depend on C for tool chains, even if most "portable" C wont run correctly on them. The fact that you can build hardware like that, and not have to invent a new language / dialect to program them is a huge win for the world.

<edit> I didnt see the footnote about DSPs at first read </edit>

flowerbreeze - 12 hours ago

Thank you for the article! Do I see a Turbo-C screenshot there or am I imagining it? It was my first IDE (I didn't know that's what it was called) when I started programming. I sometimes miss it, it was really good, especially the help system.

I agree with the article of course. I think most confusion comes not having learned about the purpose of having them be defined based on the architecture in the first place. It took me a long time before I stumbled upon how they really worked and why, because while I started it was either x86 or nothing. When x64 showed up, suddenly it became relevant and everybody started learning about C types more in depth as they ran into issues with sizeof.

Also, misuse in data protocols is where I think the bad reputation of the flexible type sizes came from. stdint was desperately needed for that reason and it came a bit late.

RobotToaster - 12 hours ago

> A 'plain' int object has the natural size suggested by the architecture of the execution environment.

Shouldn't they be 64 bits on most modern systems then?

stkdump - 8 hours ago

The problem begins when you start mixing the traditional types and (u)intN_t, because the latter are merely aliases for the internal types, and it messes up overload resolution. All relevant platforms have pretty much agreed the size of char, short (int), int and long long (int). They have different opinions about long (int) and thus an int64_t might use either long (int) or long long (int).

So the best solution for nowadays is to use just char, short, int and long long (and make strong assumptions that these are exactly 8, 16, 32 and 64 bits wide respectively), never use long or long double. Never use (u)intNN_t. Then you are good.

Those caveats of the past (but int might be 16 or 36 bits), are exactly that. An artifact of the past. A historical curiosity. Not relevant for today or the future. No, I don't believe for a second that any future platform will change their size.

Platforms also still disagree on the signedness of char, so when an 8 bit numeric type (as opposed to an ascii character type) is needed, one should always explicitly specify signed char or unsigned char, both of which are separate types from char.

Further things of note: platforms also have agreed on little endian (so called "network byte order" is dead and should never be used in new protocols, because it forces everyone to convert) and on IEEE memory representation of float and double. Contrary to popular belief the main floating point operations (+,-,*,/,==,<,>,<=,>=) are also precisely defined and always behave exactly the same (leaving out strange edge cases such as denormals). And yes, of course platforms have very long agreed on twos-complement for negative integers. This even made it into the standard at some point, I believe. Same happened with the memory layout of a vector<>, which in the past wasn't standardized, but because everyone of course did the obvious (and made it the same as a normal C array), it was added to the standard later.

What I am saying, what the C++ standard guarantees isn't everything. There are much more guarantees modern C++ code can (and should) rely on.

usrnm - 12 hours ago

But not having fixed size integers (or integers tied to the size of a pointer) was. Both can be useful

pjmlp - 12 hours ago

Kind of, the mistake was not doing like PL/I where besides default machine specific sizes, the developer could explicitly assert the required sizes.

gustavopezzi - a day ago

Author here. Thanks for sharing.

adastra22 - 11 hours ago

No one thinks that ptrdiff_t should be a fixed size. It is quite obviously the integer type you would get from subtracting two pointers, which is naturally tied to the word size of the machine you are using. C's original "int" type is what we would now call ptrdiff_t.

lexicality - 12 hours ago

I feel like the article glosses over the fact that (to my mind) `int_fast32_t` and `int_least32_t` are a much better solution than "int is a random size good luck"

If you code exclusively using those types (and the `*ptr_t` ones) then you precisely express to both the compiler and the next person reading it what is supposed to be in those variables.

spacedcowboy - 10 hours ago

So, writing xc [1], I took the opposite approach, but the real reason for that was more cross-platform compatibility - xc compiles for Mac(M series), Win64, Linux (x86_64), iOS, Android, WASM, m68k, Arm A9, and 6502. The basic types in xc are spelt {u,i}{8,16,32,64} and since the platforms covered range through 8-, 16-, 32- and 64-bit machines, being explicit about the size of the data-structure was a lot more useful than it being implied.

I can see the argument for "an int works on the natural machine size". But it starts getting a lot more complicated when you have structs - suddenly byte positions are very important (as is 'sizeof' :), and if you're running the same code on different platforms, and using pointers to access them, well you need to be careful...

Fixed-size types (and we've more or less given up on non-power-of-2 sized primitive types) force you to think about the size of the type you're using at the point of creation, and if you really do want 'an int is the size of the local machine', you're free to 'typedef u32 int;' in a platform-specific file - I deliberately did not use 'int', 'short', 'long' etc. in the language.

[1] https://compile-xc.org/compiler/language/types/

lmz - 12 hours ago

Meh. In today's world if exact sizes were not a requirement then you should use the int_fastN_t types to at least guarantee the width you are expecting instead of using the fixed size types (which may not be optimal) or using plain "int" which may be smaller than expected.

msla - 2 hours ago

Does the standard allow ALL-CAPS headers, or is that just a DOS thing? Because it seems like the C standard always specifies lower-case, but I guess DOS compilers (and, maybe, DOS users) can't distinguish between ALL-CAPS and all-lower.

imtringued - 10 hours ago

>This is a deliberate design statement. int was never meant to be "32 bits". It meant "whatever this machine is fastest and most comfortable with".

This is the issue with how people talk about C. int is basically the signed version of size_t aka a word sized data type. It's not meant to have a fixed size.

When people want the classic 4 byte data type they should choose long instead.