Because 32/32 architectures (32-bit registers, 32-bit address space) became dominant for a long time. I'm referring to the 386, 68k, and the common 32-bit ARMs.
I still remember the era when int was usually 16 bits and you needed a long to get 32. This is still true for smaller embedded systems.
You can often override that to be 8-bit on compilers for 8-bit microcontrollers, on which C's integer promotion rules to 16 bits can be costly.
I'd think that while a good compiler should be able to optimise those away, even a good programmer might not always write code that provides the compiler with the hints to do that.
In the other direction, Lattice C used 32-bit ints on the 68000, where 16-bit was more efficient. This led to a bunch of code explicitly using short instead of int, and then later being converted back to int for later platforms -- sometimes with amusing results, like a comment labeling an error code as meaning "the buffer is too int."
A related big C myth that I believed for far too long relates to structs / bitfields and how they are NOT required to be packed as tightly as possible by the spec.
Worse, offhand, I don't know of any portable / standard way to achieve that. Only compiler specific extensions. (I can hope it might be portable to E.G. clang) https://stackoverflow.com/questions/11770451/what-is-the-mea... -- struct __attribute__((packed, aligned(4)))
Which way did the myth go? I am unclear on your statement.
In my experience, which admittedly does not include all members of our industry, most people are using `packed` in the same way that Gentoo users were using `-funroll-loops`. In fact, less than 2 hours ago I had to inform my worst colleague (all time career worst) that `aligned(N)` means AT LEAST N, not exactly N. Same colleague also goes along with widely-believed error that `assume_aligned` somehow makes it safe to do incorrectly-aligned accesses, when what it really does is causes the compiler to continue instead of aborting when it sees your code which is obviously wrong.
This mindset leads to a multitude of bugs and brittle non-portable code. Almost as bad as sticking pointers into a uint32_t and breaking portability off of 32-bit platforms. int is 16-bits minimum. If 2^15-1 isn't enough range, switch to long and stop writing brittle code.
I call a bluff on that. 64KB is small enough that it's not just yet another target. It requires programming in a completely different way, one that is unacceptably ugly and contorted for 32-bit and larger targets.
If you're writing code and not specifically targetting 64KB systems, your code will be completely unusable on such systems anyway. Most programs and libraries written for larger platforms will have more code than a 16-bit target can even address.
Even if you use theoretically correct sizes, they'd still be inappropriate. 16-bit lengths are a bloat if you could fit data in 8 bits, and you'd try hard to do so. There's hardly any room for stack, so even function arguments and local variables are a luxury to be avoided. Real 16-bit programs are often just a one huge function and all global variables.
There are platforms with 16-bit int and a larger than 16-bit address bus, such as AVR and Amiga.
> Real 16-bit programs are often just a one huge function and all global variables.
I've written software for extremely resource-constrained microcontrollers (7KB flash, 256 bytes of RAM) in "normal C" with many functions, code organized in multiple files, etc. In one case it was to replace firmware written by someone with your mindset, and the resulting code was smaller while having more features.
You should really be using stdint.h types these days unless you are just writing personal stuff/toys/doing homework. Do it even in using temp variables for things like for loops. Keep things explicit, distinct and obvious. It will help you debug later on. *
The inttypes.h manpage has a rationale section that's important here.
Especially in data structures if you are hitting the disk or network - the data structure alignment can get screwed up once it hits another machine. It's not worth it; stable reliable C is hard enough already. Things tend to break in stupid ways when you do things in old school ways. It's why people replaced that stuff.
* - As an example, instead of calling the collection of a type, say "review" as say, "reviews", call it something like "reviewList" or "reviewGroup" - it's easier to scan for errors and you keep the type with the name. This pays off big time especially in larger, messier code-bases where there's multiple ways to do collections of things that only differ in dumb bug-ridden ways. You're only going to work on broken code because code that's not broken doesn't need work.
Especially in data structures if you are hitting the disk or network - the data structure alignment can get screwed up once it hits another machine.
The only integer type you should be using in data structures which are going out to disk or network is uint8_t. Otherwise the uint32_t you wrote to disk on an x86 system might end up getting read on a PDP-11 and suddenly your little-endian value is middle-endian instead.
(I would love to see the C standard grow explicitly-endian types so you could write e.g. "ubeint32_t x = 0x12345678;" and have the value serialized in big-endian form. But alas the C standard moves slowly.)
Not just sane, but fast too. Compilers have long been able to recognize this algo and will optimize all those bit ops to a single BSWAP assembly instruction as necessary.
What does it mean to have an "explicitly-endian" type? In the register, it's all the same thing.
An integer type with an explicit representation in memory. In this case, "arithmetic works like a uint32_t, but when you're storing it to memory, do it in big-endian format".
Follow the Rob Pike way [1] and regain sanity.
This is the Rob Pike way, except it would involve the compiler writing the code for converting between uint32_t and uint8_t[4].
> (I would love to see the C standard grow explicitly-endian types so you could write e.g. "ubeint32_t x = 0x12345678;" and have the value serialized in big-endian form. But alas the C standard moves slowly.)
How would that work? If it keeps that layout all the time performance will be terrible, and if you want to convert it when serializing how do you intend to trigger that conversion? (And is that trigger something you couldn't do with a library?)
For in-memory operations you would use uint32_t as normal, but for the memory structures which are going to be written to disk or sent over the network you would have ubeint32_t. When you assign the value from one to the other, the compiler would insert a byteswap if necessary.
Basically this would be a simpler replacement for explicit serialization of integers into bytes.
So I have to write two versions of the data structure? I don't know if that sounds very simple to me, but it does sound like you could do that with a library and no language changes.
You probably have two versions of the data structure anyway, since the data structure you want in memory is unlikely to be the data structure you want on disk or over the network. Your in-memory data structure probably has pointers, for example.
I prefer doing some quick sanity checks and then printing an error message like "User error: Your computer is weird. Buy a normal computer." if they fail. The convenience of being able to use memory-mapped files directly without any endianness / alignment bureaucracy is more important to me than supporting some hypothetical hardware I don't have access to.
Low-level code like that rarely lives beyond 5-10 years anyway. Even if some weird future hardware starts getting popular, the code will most likely be rewritten at least once before that hardware actually matters.
> It's not worth it; stable reliable C is hard enough already. Things tend to break in stupid ways when you do things in old school ways. It's why people replaced that stuff.
Huh? There's newer, modern C, C2x, which follows up on both C11 and C99. There's also newer modern libraries that learn from the past. GCC 9 had support for C2x a couple of years ago. You can use the stuff now, pretty safely.
The advocacy is to not program C like it's still 1988 (when the most recent K&R C went out) and keep up with the times.
The backwards compatibility of C is fantastic, really quite stunning and that is why we shouldn't be afraid of moving forward.
As far as good C being hard, do you deny this? It's not a bad thing, it acts as a competency filter. Lousy Python, Javascript, PHP - it still works but lousy C will just crash and explode. That's a feature, not a bug.
You can use it up to a point, but it's never going to be the natural way of using C. short/int/long are immediately available by default, the stdint types require an extra import and are more cumbersome. And even the most modern versions of the language are still distinctly unsafe.
> It's not a bad thing, it acts as a competency filter. Lousy Python, Javascript, PHP - it still works but lousy C will just crash and explode. That's a feature, not a bug.
Hardly. Lousy C usually silently corrupts your data (not to mention letting attackers control your computer), and often the authors never even realise it was wrong.
I started using C in the late 80s and have used C++ on and off since then. I've not been able to understand how it ever made sense to have a language where the size of int (and other types) varies depending on the platform and implementation.
I don't think I've ever been in a situation where I thought "I need this variable to have a type that's the 3rd smallest available, but I don't care how many bits it actually is. On some platforms, it will overflow at 32768, and on other platforms it will overflow at 2.1 billion, and I'm fine with either behavior."
Every time I choose a type for a variable, I'm thinking "32 bits is an appropriate size for this variable, and this other variable needs 64 bits".
I understand that these days we use int32_t etc, but why did the unpredictably-sized types ever make sense to anyone?
You're straw-manning a bit with your "3rd smallest type available." Int was supposed to be the natural word-size for the system, which, despite C being "portable," you had to know for efficiency reasons. Memory access at the time was slow, and if you specified an Int that you couldn't load/unload with a single instruction or fit into a register, you would kill your performance.
Thanks for the response. On one hand, it makes sense that the natural word size is relevant. On the other hand, that means it encourages writing non-portable code.
Int in particular has that use. But there isn't a similar value in char and short and long being variable, so I wouldn't say it's a strawman.
And int has stopped being a reliable machine word size, if it ever was reliable in the first place.
Edit: I would say the most useful sizes are fixed_width and max(fixed_width, machine_word). And none of the native C types reliably fit into either of those categories.
Actually, memory access was fast (assuming you're referring to the time when C was developed, not when ANSI C was standardized), as you could access any individual memory location in a single clock cycle. It's once you start seeing the development of RISC systems that being able to clock processors faster than memory starts becoming a thing, and that makes memory slow.
> and if you specified an Int that you couldn't load/unload with a single instruction or fit into a register, you would kill your performance.
This is the bigger issue. If you have to do lots of extra masking or adjustment of the values to emulate an N-bit integer that isn't otherwise available on your system, that's the slowdown.
If I were to speculate... I think you're really underestimating how limited hardware used to be.
Firstly, portability of code was simply not as useful a feature. The big benefit was that the programmer could read and write code anywhere, not that the same code ran everywhere. Everything you could do with a computer was fundamentally hardware-dependent. If you needed to take your code and transfer it to another platform, you were going to have to deal with such a fundamentally different architecture that you already knew you'd have to rewrite it. Like storage wasn't even compatible. You didn't even know the width of a character, let alone the encoding for it. It could be 7 bits, 6 bits, 5 bits, whatever. RS232 communication might even be a struggle. C came about in 1972. The first widely available single chip UART was in 1971.
Second, hardware was simply not capable enough to dynamically handle a fixed size. If you wanted a 16 bit integer, your hardware might not be able to do operations on that at all. You might have 6 bit integers and everything else is a bitwise stream. If you want 16 bit math, maybe you've got to figure out how to do that 6 bits at a time yourself. Some of the hardware you were dealing with was microchips, not microcontrollers.
So you didn't do it because (a) it wasn't a concern for the programmer because portability wasn't really a thing in the same terms, and (b) it was outside the practical capabilities of the hardware.
It's partly an artifact of BCPL where the only type was one representing the machine word. So with a word-sized type you don't get portability in the range of values the type can represent, but can portably know that you won't e.g. take up 2 registers by using it.
You might consider the int_fastN_t types a sort of spiritual successor to this with fewer downsides since they purposefully guarantee a minimum width.
I'm not an expert on the history, but I think older processors didn't have say a divide instruction at a bunch of different widths, there'd be one special width and everything bigger had to break out into a more complex implementation based on the divide for the smaller width. Int was supposed to be that special width that was one-to-one with underlying instructions.
Hardware divide has little to do with it. Historically, it is just the register width. Division is too complex and variable even within an ISA.
On a side note, there are still micros in common use today, such as low end Cortex that don't have any hardware divider. And the lifecycles of embedded devices mean that even older architectures, but still less than 20 years old such as ARMv5 without hardware dividers are still being maintained. It's not that historic.
The performance of hardware divide isn't something that follows simply from the "natural" register width of the processor anyway, it depends on the real estate and implementation given to it.
> I think older processors didn't have say a divide instruction at a bunch of different widths
But the common ones did, for example PPC and even the 8086. The reason for this is because division is slow and you get a benefit by using the smallest width possible.
> special width that was one-to-one with underlying instructions
That's usually just the width of the general purpose registers.
Of course 64-bit came along and threw this out the window.
> I don't think I've ever been in a situation where I thought "I need this variable to have a type that's the 3rd smallest available, but I don't care how many bits it actually is. On some platforms, it will overflow at 32768, and on other platforms it will overflow at 2.1 billion, and I'm fine with either behavior."
No, but you've probably been in a situation where "I need this variable to fit in a register", no?
Ok, then it would make sense to have ONE datatype that fits a register. But it would make more sense that the other types (char, short, long) are a known and fixed size. Otherwise, what's the use case for short? "I need something that's smaller than a register, but I don't care what range it can store?"
FWIW "short" has always been 16 bits. It was introduced with VAX Unix (and a few other 32 bit architectures) so that you could easily port code that assumed 16 bit arithmetic.
The point isn't that, today, you'd choose to do what C did. But in 1977 there weren't a lot of other options. Early C did indeed have fixed, defined types. But moving to the VAX messed all that up. They had to choose to either add new types (and thus turn a bunch of code that would otherwise run fast on the VAX into weirdly-interpreted nonsense) or update the existing types so that idiomatic PDP-11 code still did what you expect most of the time. They chose the latter.
I agree. And I find it interesting that everyone seems to think the alternative is having a handful of hard coded sizes (8, 16, 32, 64). I don't think that's the answer either.
I imagine a language without any concrete integer type, instead you have to parameterize.
For example int<32> for a 32 bit integer. Syntax is debatable but that's the idea.
It's great cuz it can work on even weird hardware, I can make an int<13> for 13 bit machines.
And you can make typedefs for common stuff. Like a size_t mapping to int<32> or a word_t mapping to int<16>. Whatever you need.
I've been meaning to make a compiler to test the idea out. Someday maybe
Yes, and I'm excited about that! Though I do wish the syntax sucked less. Also would be interesting to see it be the primary integer type of the language.
I'm also interested in a more advanced version for setting arbitrary bounds. E.g. an integer that goes from 1-10. Adding two of those would be an integer from 2-20. The goal being that the compiler could help enforce that you don't pass a value that is OOB. Might make more sense for that to be a library. Perhaps some C++ template magic. But I digress
In the 1970s, the size of a byte changed from platform-to-platform, with 6-bit bytes and 6-word (aka: 36-bit words) remaining popular. It is in this environment that C was invented, or at least its predecessor the B language as well as Unix.
By the 1980s, C is getting standardized but I'm guessing programmers were still used to the different sized bytes (let alone different sized words and/or integers) that 4x8-bit vs 6x6-bit (and other such computers). True, the 1980s established the 8-bit standard, but a veteran programmer had no reason to believe that 8-bits was going to remain the standard for the next decade.
In the beginning was int, which was a 16-bit integer, because that was the size of a register on PDP-11. Then char was added to represent individually addressable bytes in memory.
The rationale explains that long was added to be able to represent 32-bit disk offsets. Since 32-bit arithmetic is slower on 16-bit systems (and 16-bit arithmetic can be slower on 32-bit systems!), you don't want to use that, so the solution was to add a 16-bit short, 32-bit long, and an int that is whichever is faster. The types aren't actually mandated to be 16-bit and 32-bit respectively because that would be awkward on existing 24-bit or 36-bit machines. In a mirror to this problem, the need by C99 for 64-bit offsets on 32-bit systems, combined with a serious existing corpus of code meant that you needed to create a new type for 64-bit, which was long long. But now you get the additional fun that it's not clear on a 64-bit system if long is 32 bits or 64 bits.
In modern practice, most hardware is 32-bit or 64-bit (and anything that isn't pretty much requires code to be written from scratch specifically for it anyways), and all 64-bit processors make it no less performant to use 32-bit than 64-bit for computation. Thus i32-everywhere gives you both consistency and performance portability in a way that wasn't true in the 16-to-32 transition era.
Of course, by the time you have the varying sized insanity, people who wanted code to be portable instead actually defined their own fixed-size types, which C99 also standardized into stdint.h.
Good timing! I was having a conversation today with someone on a CUDA discord about it. The CUDA examples generally use `int` for the pointer size. I'd assumed it was a shorthand for 32-bit-unsigned integer from the context, but it's apparently a standin that the compiler attempts to do the right thing with.
So, it's often used as you'd use `usize` in Rust, but not always! For now, my model is: use `int` where you would use `usize` in rust. It can also be used for other things, but it's better to use `uint32_t` or another explicitly-sized type.
That's a really bad idea. The C equivalent to `usize` is `uintptr_t` or `size_t` [1]. `int` is a 32-bit integer on all common platforms, while `long` is 32-bit on some 64-bit platforms and 64-bit on others. But on 64-bit platforms, `int` is not going to be large enough to store a pointer.
[1] There's a subtle distinction between the two of them, but on most practical architectures you're coming across now, size_t == uintptr_t. This is actually a bit of a fight right now in Rust, since there's disagreement on what `usize` should do on systems where size_t != uintptr_t (the most common of which is CHERI).
52 comments
[ 3.4 ms ] story [ 116 ms ] threadI still remember the era when int was usually 16 bits and you needed a long to get 32. This is still true for smaller embedded systems.
Worse, offhand, I don't know of any portable / standard way to achieve that. Only compiler specific extensions. (I can hope it might be portable to E.G. clang) https://stackoverflow.com/questions/11770451/what-is-the-mea... -- struct __attribute__((packed, aligned(4)))
In my experience, which admittedly does not include all members of our industry, most people are using `packed` in the same way that Gentoo users were using `-funroll-loops`. In fact, less than 2 hours ago I had to inform my worst colleague (all time career worst) that `aligned(N)` means AT LEAST N, not exactly N. Same colleague also goes along with widely-believed error that `assume_aligned` somehow makes it safe to do incorrectly-aligned accesses, when what it really does is causes the compiler to continue instead of aborting when it sees your code which is obviously wrong.
Don't you simply make sure all bit fields are adjacent?
https://beej.us/guide/bgc/html/split-wide/structs-ii-more-fu...
If you're writing code and not specifically targetting 64KB systems, your code will be completely unusable on such systems anyway. Most programs and libraries written for larger platforms will have more code than a 16-bit target can even address.
Even if you use theoretically correct sizes, they'd still be inappropriate. 16-bit lengths are a bloat if you could fit data in 8 bits, and you'd try hard to do so. There's hardly any room for stack, so even function arguments and local variables are a luxury to be avoided. Real 16-bit programs are often just a one huge function and all global variables.
> Real 16-bit programs are often just a one huge function and all global variables.
I've written software for extremely resource-constrained microcontrollers (7KB flash, 256 bytes of RAM) in "normal C" with many functions, code organized in multiple files, etc. In one case it was to replace firmware written by someone with your mindset, and the resulting code was smaller while having more features.
The inttypes.h manpage has a rationale section that's important here.
Especially in data structures if you are hitting the disk or network - the data structure alignment can get screwed up once it hits another machine. It's not worth it; stable reliable C is hard enough already. Things tend to break in stupid ways when you do things in old school ways. It's why people replaced that stuff.
* - As an example, instead of calling the collection of a type, say "review" as say, "reviews", call it something like "reviewList" or "reviewGroup" - it's easier to scan for errors and you keep the type with the name. This pays off big time especially in larger, messier code-bases where there's multiple ways to do collections of things that only differ in dumb bug-ridden ways. You're only going to work on broken code because code that's not broken doesn't need work.
The only integer type you should be using in data structures which are going out to disk or network is uint8_t. Otherwise the uint32_t you wrote to disk on an x86 system might end up getting read on a PDP-11 and suddenly your little-endian value is middle-endian instead.
(I would love to see the C standard grow explicitly-endian types so you could write e.g. "ubeint32_t x = 0x12345678;" and have the value serialized in big-endian form. But alas the C standard moves slowly.)
If I'm going to crack open an int32, it's htonl all day long and ntohl it back when I'm done.
I have seen tons of code with #if BIG_ENDIAN ... and it's a huge nightmare. Follow the Rob Pike way [1] and regain sanity.
[1] https://commandcenter.blogspot.com/2012/04/byte-order-fallac...
An integer type with an explicit representation in memory. In this case, "arithmetic works like a uint32_t, but when you're storing it to memory, do it in big-endian format".
Follow the Rob Pike way [1] and regain sanity.
This is the Rob Pike way, except it would involve the compiler writing the code for converting between uint32_t and uint8_t[4].
How would that work? If it keeps that layout all the time performance will be terrible, and if you want to convert it when serializing how do you intend to trigger that conversion? (And is that trigger something you couldn't do with a library?)
Basically this would be a simpler replacement for explicit serialization of integers into bytes.
Low-level code like that rarely lives beyond 5-10 years anyway. Even if some weird future hardware starts getting popular, the code will most likely be rewritten at least once before that hardware actually matters.
If this is your mentality then why use C at all?
The advocacy is to not program C like it's still 1988 (when the most recent K&R C went out) and keep up with the times.
The backwards compatibility of C is fantastic, really quite stunning and that is why we shouldn't be afraid of moving forward.
As far as good C being hard, do you deny this? It's not a bad thing, it acts as a competency filter. Lousy Python, Javascript, PHP - it still works but lousy C will just crash and explode. That's a feature, not a bug.
You can use it up to a point, but it's never going to be the natural way of using C. short/int/long are immediately available by default, the stdint types require an extra import and are more cumbersome. And even the most modern versions of the language are still distinctly unsafe.
> It's not a bad thing, it acts as a competency filter. Lousy Python, Javascript, PHP - it still works but lousy C will just crash and explode. That's a feature, not a bug.
Hardly. Lousy C usually silently corrupts your data (not to mention letting attackers control your computer), and often the authors never even realise it was wrong.
I don't think I've ever been in a situation where I thought "I need this variable to have a type that's the 3rd smallest available, but I don't care how many bits it actually is. On some platforms, it will overflow at 32768, and on other platforms it will overflow at 2.1 billion, and I'm fine with either behavior."
Every time I choose a type for a variable, I'm thinking "32 bits is an appropriate size for this variable, and this other variable needs 64 bits".
I understand that these days we use int32_t etc, but why did the unpredictably-sized types ever make sense to anyone?
Int would be whatever the instruction set of the platform supports by default for most of its arithmetic and memory operations.
C would be sitting a layer above assembly but not try to hide what is going on behind the scene I guess.
Much later on languages like java would bring the concept of virtual machines where the types are unified across different instructions set.
And int has stopped being a reliable machine word size, if it ever was reliable in the first place.
Edit: I would say the most useful sizes are fixed_width and max(fixed_width, machine_word). And none of the native C types reliably fit into either of those categories.
Actually, memory access was fast (assuming you're referring to the time when C was developed, not when ANSI C was standardized), as you could access any individual memory location in a single clock cycle. It's once you start seeing the development of RISC systems that being able to clock processors faster than memory starts becoming a thing, and that makes memory slow.
> and if you specified an Int that you couldn't load/unload with a single instruction or fit into a register, you would kill your performance.
This is the bigger issue. If you have to do lots of extra masking or adjustment of the values to emulate an N-bit integer that isn't otherwise available on your system, that's the slowdown.
Firstly, portability of code was simply not as useful a feature. The big benefit was that the programmer could read and write code anywhere, not that the same code ran everywhere. Everything you could do with a computer was fundamentally hardware-dependent. If you needed to take your code and transfer it to another platform, you were going to have to deal with such a fundamentally different architecture that you already knew you'd have to rewrite it. Like storage wasn't even compatible. You didn't even know the width of a character, let alone the encoding for it. It could be 7 bits, 6 bits, 5 bits, whatever. RS232 communication might even be a struggle. C came about in 1972. The first widely available single chip UART was in 1971.
Second, hardware was simply not capable enough to dynamically handle a fixed size. If you wanted a 16 bit integer, your hardware might not be able to do operations on that at all. You might have 6 bit integers and everything else is a bitwise stream. If you want 16 bit math, maybe you've got to figure out how to do that 6 bits at a time yourself. Some of the hardware you were dealing with was microchips, not microcontrollers.
So you didn't do it because (a) it wasn't a concern for the programmer because portability wasn't really a thing in the same terms, and (b) it was outside the practical capabilities of the hardware.
You might consider the int_fastN_t types a sort of spiritual successor to this with fewer downsides since they purposefully guarantee a minimum width.
On a side note, there are still micros in common use today, such as low end Cortex that don't have any hardware divider. And the lifecycles of embedded devices mean that even older architectures, but still less than 20 years old such as ARMv5 without hardware dividers are still being maintained. It's not that historic.
The performance of hardware divide isn't something that follows simply from the "natural" register width of the processor anyway, it depends on the real estate and implementation given to it.
> I think older processors didn't have say a divide instruction at a bunch of different widths
But the common ones did, for example PPC and even the 8086. The reason for this is because division is slow and you get a benefit by using the smallest width possible.
> special width that was one-to-one with underlying instructions
That's usually just the width of the general purpose registers.
Of course 64-bit came along and threw this out the window.
No, but you've probably been in a situation where "I need this variable to fit in a register", no?
The point isn't that, today, you'd choose to do what C did. But in 1977 there weren't a lot of other options. Early C did indeed have fixed, defined types. But moving to the VAX messed all that up. They had to choose to either add new types (and thus turn a bunch of code that would otherwise run fast on the VAX into weirdly-interpreted nonsense) or update the existing types so that idiomatic PDP-11 code still did what you expect most of the time. They chose the latter.
I imagine a language without any concrete integer type, instead you have to parameterize. For example int<32> for a 32 bit integer. Syntax is debatable but that's the idea.
It's great cuz it can work on even weird hardware, I can make an int<13> for 13 bit machines.
And you can make typedefs for common stuff. Like a size_t mapping to int<32> or a word_t mapping to int<16>. Whatever you need.
I've been meaning to make a compiler to test the idea out. Someday maybe
I'm also interested in a more advanced version for setting arbitrary bounds. E.g. an integer that goes from 1-10. Adding two of those would be an integer from 2-20. The goal being that the compiler could help enforce that you don't pass a value that is OOB. Might make more sense for that to be a library. Perhaps some C++ template magic. But I digress
By the 1980s, C is getting standardized but I'm guessing programmers were still used to the different sized bytes (let alone different sized words and/or integers) that 4x8-bit vs 6x6-bit (and other such computers). True, the 1980s established the 8-bit standard, but a veteran programmer had no reason to believe that 8-bits was going to remain the standard for the next decade.
In the beginning was int, which was a 16-bit integer, because that was the size of a register on PDP-11. Then char was added to represent individually addressable bytes in memory.
The rationale explains that long was added to be able to represent 32-bit disk offsets. Since 32-bit arithmetic is slower on 16-bit systems (and 16-bit arithmetic can be slower on 32-bit systems!), you don't want to use that, so the solution was to add a 16-bit short, 32-bit long, and an int that is whichever is faster. The types aren't actually mandated to be 16-bit and 32-bit respectively because that would be awkward on existing 24-bit or 36-bit machines. In a mirror to this problem, the need by C99 for 64-bit offsets on 32-bit systems, combined with a serious existing corpus of code meant that you needed to create a new type for 64-bit, which was long long. But now you get the additional fun that it's not clear on a 64-bit system if long is 32 bits or 64 bits.
In modern practice, most hardware is 32-bit or 64-bit (and anything that isn't pretty much requires code to be written from scratch specifically for it anyways), and all 64-bit processors make it no less performant to use 32-bit than 64-bit for computation. Thus i32-everywhere gives you both consistency and performance portability in a way that wasn't true in the 16-to-32 transition era.
Of course, by the time you have the varying sized insanity, people who wanted code to be portable instead actually defined their own fixed-size types, which C99 also standardized into stdint.h.
So, it's often used as you'd use `usize` in Rust, but not always! For now, my model is: use `int` where you would use `usize` in rust. It can also be used for other things, but it's better to use `uint32_t` or another explicitly-sized type.
[1] There's a subtle distinction between the two of them, but on most practical architectures you're coming across now, size_t == uintptr_t. This is actually a bit of a fight right now in Rust, since there's disagreement on what `usize` should do on systems where size_t != uintptr_t (the most common of which is CHERI).