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.

  • locknitpicker 12 hours ago

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

    This is a highly ignorant comment. You're confusing the fact that you only had to work with a single target architecture with the whole concept of multiple processor architectures being somehow obsolete, as if there was a sudden law of nature that forced every single computer, being full blown HPC stuff or small microcontrollers used in embedded applications.

    Take a look at arduino. They still have 16-bit models out there. Also noteworthy, it seems some DSPs also have ints larger than 32 bits.

    • flohofwoe 12 hours ago

      The parent is completely right in the sense that for actually portable C code it was always better to use fixed-width integer types which were chosen for the problem to solve instead of target hardware capabilities.

      For instance if your integer arithmetic needs to happen with 32 bit precision (no matter if the code runs on a 16- or 32-bit CPU), there is no scenario where using 'int' makes sense. Instead you'd use a fixed-width 32-bit integer type and accept that math operations are compiled into two instructions on a 16-bit CPU.

      And OTH if you only require 16 bits integer width, there's not much point in picking a 32 bit integer type. Since two's-complement integer encoding has been standard since at least the 70s, the CPU can do narrow operations in the native register width. Any overflow/wraparound is still correct when only looking at the lowest 16-bits of the result.

      • locknitpicker 10 hours ago

        > The parent is completely right in the sense that for actually portable C code it was always better to use fixed-width integer types which were chosen for the problem to solve instead of target hardware capabilities.

        You're confusing things. It's one thing to claim that either they never used a feature or they even have a personal preference to do things one way or another.

        Another entirely different thing is to proclaim a programming language designed to target any conceivable CPU architecture somehow no longer needs to support basic cpu arch traits such as word size.

        As I pointed out,there are still processors being sold today that do not support 32-bit ints. If you expect C to be able to target these architectures, obviously this feature is still a critical feature.

        Also, people who maintain yesterday's systems that require non-32bit ints still need to work on them.

        Chesterton's fence is still relevant. Why are we pretending that it's ok to mindlessly proclaim a feature is not requires because we don't understand why it was necessary to begin with?

        • reamaer 1 hour ago

          To inform GP (and me) I interogated LLm a bit :

          word sized variable is relevant for performance, it is processed in exactly one cycle (I strongly suspect there is an asterix somewhere).

          Most notable uses, where int makes sense over int32_t: array indexes, for-loop variables, enum, flags.

      • jabl 1 hour ago

        > And OTH if you only require 16 bits integer width, there's not much point in picking a 32 bit integer type.

        Some common architectures like x86 can suffer from an issue called partial register stalls. So from a performance perspective choosing a 32 bit integer can be better.

    • adrian_b 12 hours ago

      As I have said, I have not worked with a single architecture.

      Before 1990, I had worked with a variety of ISAs, from IBM mainframes and DEC minicomputers to many kinds of microprocessors.

      After 1990, I have used C on a great variety of x86, Motorola 68xxx, IBM/Motorola PowerPC and many generations of ARM ISAs.

      Even if you use explicit 32-bit integers in a program, that will not create any correctness problem when the program is run on 16-bit microcontroller. At most such a program may have a suboptimal performance. Performance problems are much easier solved during porting than obscure bugs.

      There have been some popular DSPs with 24-bit integers, e.g. Motorola 56xxx. Nonetheless, nobody would want to run on such a DSP a program that was written for another kind of CPU, even for another kind of DSP, because the performance would be pathetic. Any program for such a fixed-point DSP, even when derived from an existing program, would need to be rewritten while using at every point in the program the knowledge that the size of "int" is 24 bits (because the programs for fixed-point DSPs need copious amounts of scaling operations, to avoid overflows and underflows), so such a program should not actually use "int", but it should typedef an "int24_t", to make this assumption explicit.

  • jibal 11 hours ago

    Per the C standard `sizeof(char)` is always 1, regardless of how many bits it has.

    • ddlsmurf 11 hours ago

      because it's the unit of addressable memory that C is concerned by with sizeof, otherwise its values still depend on CHAR_BIT

  • nickcw 11 hours ago

    The fixed size int types int32_t and friends weren't introduced until C99. Microsoft held out until 2013 before it put <inttypes.h> into Visual Studio!

    So there has been a really long time in C's evolution where we haven't had fixed size types which has been a super annoying mess of #ifdefs in portable code.

    The variable size ints have allowed some super weird architectures though. I remember looking at the datasheet for the Motorola 56000 DSP and noting that the C compiler set char = short = int = 24 bits! That was because the hardware could not address anything smaller than 24 bits. I think long could be 48 bits.

    • ThrowawayB7 1 hour ago

      The product is named Visual C++, that it happened to do C was always kind of a sideshow as far as I could tell. Aside from that, I seem to recall that VC++ had the WORD, DWORD, and eventually QWORD macros to specify unsigned 16, 32 and 64 bit types respectively.

    • convolvatron 1 hour ago

      I always just defined these types in a per-platform portability header myself.

  • ddlsmurf 11 hours ago

    I would agree if overflow on those types wasn't undefined behaviour, or unpredictable

  • bobmcnamara 2 hours ago

    Plus char could be signed or unsigned!

nayuki 12 minutes ago

Some pieces of code in the article look suspicious:

    #define LUAI_IS32INT ((UINT_MAX >> 30) >= 3)

If unsigned int is just 16 bits wide, then `65535 >> 30` shifts by more positions than the width of the type, which should be undefined behavior, right?

    #define NB CHAR_BIT
    #define MC ((1 << NB) - 1)

If we have a DSP machine (as mentioned in the article) where CHAR_BIT is 16 and int is 16 bits, then `1 << NB` is `1 << 16` which shifts as much as the width of the type, which I believe is undefined behavior as well.

Regardless of whether you think C's flexible integer sizes are good or bad, and whether it helped propagate the language, there's no denying that if you want to write portable code across machines, you have to put in more effort compared to a language with fixed-size integers. Whether this matters or not is a matter of situation and opinion.

Other than that, I made a simulator where you can set the bit width of each integer type, and then it shows you how `x operation y` gets promoted to some output integer type. https://www.nayuki.io/page/summary-of-c-cpp-integer-rules section "Conversion rules simulator"

layer8 3 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”.

InvisibleUp 1 hour 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 1 hour 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.

  • sparkie 33 minutes ago

    `int` being 32-bits on amd64 was the correct decision, even in hindsight. If you are familiar with the ISA you will understand this. Existing 32-bit code just worked on the 64-bit chip, because the instruction encodings for 32-bit are unchanged. The 64-bit instructions are basically "opt-in", by placing a REX prefix on them - that makes them more expensive to encode and uses more instruction cache. Even today, compilers will emit 32-bit versions of instructions when the upper 32-bits are not needed, because it's cheaper.

    When amd64 was released, x86 was almost ubiquitous on desktops and ran the majority of servers - most of the software used by the world could continue being used. If AMD had not gone through this effort to make it backward compatible, it's likely IA64 would've won and we wouldn't have this debate. Hard to understate the importance of not breaking things.

    If you were designing a greenfield 64-bit ISA, then yes, it might make sense to have `int` be 64-bits, but it was definitely not, and still is not a mistake that it's 32-bits on amd64.

    On RISC-V for example, it's questionable. The RV32 ecosystem is tiny and almost irrelevant - if they decided to break things for RV64 it wouldn't be a big problem - probably better to fix any problems early rather than hold baggage to run software that never existed - though it's much easier to port software to RV64 if `int` is still 32-bits.

codedokode 1 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 13 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>

  • pjmlp 12 hours ago

    C survived because UNIX carried it.

    • flohofwoe 12 hours ago

      I don't agree. UNIX was an extremely niche operating system until Linux won the data center, at that time both C and C++ were already extremely popular outside the UNIX world. C won because it was so easy to adapt to new hardware architectures (even GPU shading languages are just minimally extended flavours of C and C++).

      • prerok 12 hours ago

        Sorry, what? Most of data centers were running a UNIX operating system back in the day. What operating system do you think they were running?

        • fragmede 11 hours ago

          WindowsNT was the other operating system of that time.

          • pjmlp 8 hours ago

            With Win32, OS/2 and POSIX subsystems.

          • prerok 7 hours ago

            That's way way later. UNIXes were the ones running in 70s and 80s. WindowsNT and Linux only later came to take a slice and, still later on, Linux won the day. How anyone would consider UNIX "niche" is beyond me.

            • fragmede 25 minutes ago

              In the other direction then, IBM mainframes.

        • tialaramex 11 hours ago

          There is a weird moment toward the end of the 1990s when Microsoft is trying to show their NT is a serious competitor in this space.

          Traditionally this was a profitable niche, Microsoft would like to take a fat piece of that, and instead what happened is that Linux destroyed the profit margin. A million dollars overhead that would have kept a hungry UNIX® vendor alive on your project didn't turn into an extra million dollars on Microsoft's balance sheet, instead it evaporated because Linux is "free". And so then Microsoft lost interest.

          • pjmlp 8 hours ago

            Yeah, and one of the ways to show it was a serious OS for DoD projects was to have a POSIX subsystem, which had they kept it around, Linux would never have taken off on the PC, and there would be no need for WSL 40 years later.

            • tialaramex 7 hours ago

              The POSIX subsystem is a box checking exercise. The reason the box was there isn't satisfied but the box was checked and Microsoft hoped that's good enough. If you need "a Unix" and they give you NT and circle the stuff about POSIX you don't go "Oh, perfect" you ask them to fix the requirements document so that you can have an actual Unix next time.

              WSL is Redmond going OK yeah, here you go, an actual Unix.

              • pjmlp 4 hours ago

                There was also SUA and Interix in the meantime, before WSL came to be.

                The point still stands, that POSIX checkbox was relevant enough for spending the money in engineers salary during Windows NT 3.51 development.

      • pjmlp 11 hours ago

        Most people only cared about C, because they needed to work on UNIX, and UNIX was taking over the server room and all 1980's graphical workstations.

        C was pretty much ignored on 8 bit home computers, outside some toy compilers for CP/M.

        In the 16 bit days, it was yet another language alongside BASIC compilers, Pascal, Modula-2, Assembly.

        C is so tied to UNIX, that POSIX had to be created so that any non-UNIX operating system could provide a cozy home for their C compilers.

        UNIX/POSIX is for all practical purposes the runtime most C applications rely on, there are naturally some exceptions like free-standing or Windows (which eventually gave up and add to start improving its support).

        It is only due to historical accident that Microsoft gave up on Xenix, instead of replacing their MS-DOS efforts.

        • jstimpfle 3 hours ago

          I don't know about you but I need zero POSIX to write C on Windows. Not even much of the (mostly bad) C standard library. I use snprintf for convenience (but I don't have to), and memcpy, that's about it.

          And many projects properly abstract their OS layer so they aren't tied to POSIX.

          Maybe Unix is tied to C, but C isn't tied to Unix.

      • imtringued 11 hours ago

        Disclaimer: This is just some cursory research using LLMs.

        C was invented to rewrite UNIX in a programming language that made it easy to port UNIX between machines.

        So what you're saying is contradictory. You're saying the underlying motivation of C was wrong or unnecessary (porting UNIX to different hardware architectures) but C won because that underlying motivation (easy porting between hardware architectures) was partially right.

        Your position is now that C didn't need UNIX as a stopgap, which is weird because your argument gains no weight (basically saying C's dominance is sheer coincidence) if it's true but if it's false you're just plain wrong.

        • flohofwoe 10 hours ago

          My point is that C's popularity quickly outgrew the popularity of UNIX, especially during most of the 1990s before Linux made UNIX accessible to us "PC peasants". Most 1990s PC games were written in C, and C was also the dominant high level language on 16/32 bitters like the Amiga or Atari ST.

          • pjmlp 8 hours ago

            Nope, they were mostly written in Assembly.

            On the consoles it took until PlayStation for C to take off among game devs.

            Additionally many Amiga games used Blitz BASIC and AMOS.

            Anyone involved in the Demoscene early days would be 100% Assembly as well.

            On PC, it required until Watcom with its great MS-DOS extender for devs to finally move away from Assembly in mass.

            • flohofwoe 8 hours ago

              > Nope, they were mostly written in Assembly.

              I was there, Gandalf ;)

              (and note how I specifically wrote "dominant high level language", not "dominant language", since assembly coding was indeed very relevant on those machines, for UI apps 100% assembly was quite rare though, and hybrid C/ASM seems to have been more common).

              • pjmlp 5 hours ago

                Dominant in which part of the planet?

                In my part of the Iberian Penisula it was Turbo Pascal on PC and AMOS/Blitz Basic on Amiga.

                With lots of inline Assembly anyway.

                • jstimpfle 2 hours ago

                  If id Software is any indication, apparently they wrote their games in C since their inception in 1990 with Commander Keen.

      • jjav 4 hours ago

        > UNIX was an extremely niche operating system until Linux won the data center

        UNIX was ubiquitous in the data center well before Linus even posted his first version of Linux on USENET.

        Everything ran on SunOS, HP-UX, IRIX, AIX, etc.

      • ykonstant 2 hours ago

        > UNIX was an extremely niche operating system until Linux won the data center

        ?!? What a claim!

  • adrian_b 11 hours ago

    This kind of flexibility is a purely historical thing.

    On modern computers, it is impossible to write correct C programs that are agnostic about the true size in bits of the "flexible" types char, short, int, long and long long.

    If your program must depend on assumptions about the size in bits of the integer types, those assumptions must be made explicit, by using types like int16_t, int32_t etc.

    Writing correct programs that are agnostic about the integer sizes is possible only in programming languages that allow the programmer to install an integer overflow handler even if the CPU does not generate a hardware exception for that, in which case the compiler must insert appropriate overflow checking instructions that would invoke the installed handler when necessary.

    This problem did not exist on old computers, where there were hardware exceptions for integer overflows, so even in C you could install a signal handler for SIGFPE, which would also be invoked by integer overflows.

    • quelsolaar 3 hours ago

      That's not true. All the basic types have minimum sizes, so its perfectly reasonable to write software that stays within those bounds. You can avoid any overflows if you know the minimum limits.

      • adrian_b 1 hour ago

        If you use the minimum sizes, your program will be very inefficient on most CPUs.

        I have never considered that this is an acceptable solution, which is why in decades of using C, during which I had many times to port programs or even entire real-time operating systems between different ISAs, I have never used those minimum sizes.

        Instead of having those minimum sizes, which I consider useless, C should have had since the beginning, besides sizeof, which gives the size ratio between another type and char, another operator or macro to provide the size in bits of any integer type.

        With that, it would have been possible to use types like short, int or long in a portable way.

        Nowadays, there are _WIDTH, _MAX and _MIN constants for the integer types, but those have been added relatively late to the language, together with the integer types with specified width, for which those constants are superfluous.

        • spc476 1 hour ago

          The _MAX and _MIN, along with CHAR_BIT, was defined for C89 (the first standard for C) 37 years ago, so it was possible to choose the appropriate type. I recall using the C preprocessor to define fixed (or at least, minimum) types for needed values:

              #include <limits.h>
              #if INT_MIN == 32767 && INT_MIN >= 2147483647L
              # error too small
              #else
              # error just right
              #endif
          

          It was C99 (27 years ago) where we got the fixed sized integers. So how do you define "relatively" here?

  • imtringued 11 hours ago

    Not sure why you would need to invent an new programming language when we're still strictly talking about data types. You just introduce a new data type for the hardware if that's what's necessary. You don't need a whole language.

    This is why I think so many C developers have no clue what they are doing. They just take whatever decision was made in C as gospel instead of thinking of everything being up for negotiation.

flowerbreeze 13 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.

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.

  • flohofwoe 12 hours ago

    C99 kinda fixed that with the `(u)int_leastN_t` types (which are hardly used in practice though). And shame that it took Microsoft 16 years to even start supporting C99 though so we were basically forced to keep using our own custom integer typedefs long after the C standard had fixed the issue.

    • pjmlp 11 hours ago

      Agreed, but it could have been there since day one, given the languages in 1960's.

      Well, Microsoft has considered C done for quite some time, and after C++20, they don't seem to be in a hurry to keep up with ISO either for C or C++ (there are discussions on support channels about customer relevant C++23 and C++26 features, none on C past C17), similar to how Apple and Google are handling their in box compilers as well.

  • Alpha3031 12 hours ago

    I thought most implementations of C have stdint (intN_t, leastN_t and fastN_t etc).

    • flohofwoe 12 hours ago

      These are C99 features which MSVC only got around 2015 (of course a decade later those are safe to use in portable code).

  • astrobe_ 12 hours ago

    Is that really that much of a big deal, though? Before stdint, if one needed that level of accuracy, one would do your own equivalent of stdinit by hand, and adjust those definitions when porting to another compiler/platform. The same goes for your local boolean type.

    I think the only real annoyance is that each programmer/team did it with their own convention (I32, INT32, i32, int32, WORD, Word bool, BOOL, Bool, etc., etc.); standardizing helps with putting everyone on the same page more than it helps porting. It doesn't prevent people from reverse-typedef-ing standard names to local "dialectal" names, though.

    But I also think that one should only rarely use raw integer types, in an ideal world; the elephant in the room is that typedef is kind of the second "billion dollars mistake" [1]. C is a weakly typed language and there's no practical way to undo it (besides transpilation), so there's double no point to leave behind raw types.

    [1] For those not too familiar with C, typedef defines a "type alias", not a type: https://en.cppreference.com/cpp/language/typedef

    • pjmlp 11 hours ago

      The deal was dealing with #ifdef spaghetti to define all of those, especially when mixing libraries across platforms.

      • ykonstant 2 hours ago

        Ifdef soup is still horrible, so those times (which I didn't experience firsthand) must have been abysmal.

        • sparkie 2 minutes ago

          If you want to know how bad it can be, only need to take a look at GCC's implementation of `stddef.h` for `size_t` and `ptrdiff_t` etc.

          https://github.com/gcc-mirror/gcc/blob/master/gcc/ginclude/s...

          There's a much nicer way to define these types without any ifdef soup in newer versions of C which have `typeof`, we can take advantage of the fact that `sizeof()` always returns a `size_t` and `ptr - ptr` always returns a `ptrdiff_t`. Also a L'c' literal has type `wchar_t`, so we can use that too.

              typedef typeof(sizeof(0)) size_t;
              typedef typeof(nullptr-nullptr) ptrdiff_t;
              typedef typeof(L'\0') wchar_t;
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?

  • flohofwoe 12 hours ago

    It should indeed, and in hindsight it would have been better to move int to 64 bits (especially for C's integer promotion, which only really makes sense when the promotion happens to the register width.

    But porting 32-bit code to 64-bit was a big deal back then, and C99 with its new fixed-width integer types overlapped with the first AMD64 CPUs (and Microsoft's MSVC didn't start to support C99 until around 2015 anyway), I guess keeping int on 32-bits in the popular compilers was deemed 'safer' for porting existing code. I guess we can already be lucky that all the big compilers agreed on the same int width.

    • astrobe_ 12 hours ago

      I've always believed that they kept 32 bits ints on 64 bits CPU as a default because going full 64 bits would make the code and data structures bigger for "no reason" (it's not often that one hits the 4.10^9 limit in system code (if you don't count timestamps, that is)). For instance, a load-register-with-immediate instruction would normally take 5 bytes (opcode+value) on 32 bits, but 9 bytes on 64.

      • sparkie 10 hours ago

        I think backward compatibility was the main aim. Intel tried redesigning the architecture as 64-bit native (Itanium), but AMD done a better job at backward compatibility - and intel eventually adopted it as x86-64.

        The amd64 design could run most 32-bit code with minimal changes. All the 32-bit instructions had the same encoding, besides push/pop which instead acted on 64-bits. The 64-bit instructions were basically opt-in, though a few opcodes (0x40..0x4F) had to be deprecated for the REX prefix.

      • Joker_vD 2 hours ago

        > would make... data structures bigger for "no reason"

        This was the main reason that in stayed 32 bits. Not because larger footprint of structs would be catastrophic — it wouldn't: the 64-bit migration was motivated mainly by the fact that the 32-bit architectures couldn't easily address all that actually existing physical memory past 4 GiB (and virtual memory as well) — but because lots and I mean lots of on-disk data structures were defined in terms of char/short/int, people routinely dumped/gulped their data structures as-is, with no marshalling, so changing the size of int would break literally everything that worked with on-disk data, starting with the filesystem implementations themselves.

  • userbinator 12 hours ago

    On x86-64, you need an extra prefix to do 64-bit operations (while 64-bit addressing is the default), so it's a question of "are you sure you need the 64 bits and 32 isn't enough?"

  • Alpha3031 12 hours ago

    Memory addresses being 64 bits due to needing to address more than 4 GiB memory doesn't mean most integer instructions operate most efficiently with 64 bits. Instructions for 32 bit integers are still more efficient than 64 bit, whereas 16 bit operands require a prefix byte meaning they're less compact and cache efficient (on AMD64 anyway).

  • entrope 11 hours ago

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

    Arguably so, but then one would lose the ability to natively name 16-bit integer types because "short" would be 32 bits.

    An earlier comment addresses x86-64. AArch64 (pedantically, the A64 instruction set used for AArch64's 64-bit execution mode) is similar, in that addresses are 64 bits wide but ALU instructions typically encode a width bit, called "sf", that selects either 32- or 64-bit data registers and arithmetic. See, for example, https://arm.jonpalmisc.com/latest_aarch64/add_addsub_ext .

    • imtringued 11 hours ago

      char is at least 8 bits, short is at least 16 bits, long is at least 32 bits, long long is at least 64 bits.

      Not sure how what you said makes sense.

      • debugnik 2 hours ago

        long can't be smaller than int, so a 64-bit int leaves short as the only type between char and long long, and you need to pick whether it's 16 bits or 32.

        Then again, we'd still have stdint around. And ultimately this doesn't matter because most code isn't portable anyway.

        • matvore 1 hour ago

          You would still want a native 16 bit type in order to have a pointer to a 16-bit memory location, including an array of 16-bit values.

          • debugnik 1 hour ago

            I don't think of the stdint types as being less native than the keyword integer types. C# for example makes keyword types aliases to System.* types, not the other way around.

            • matvore 1 hour ago

              I think you would need to have a compiler intrinsic type which is 16 bits. That is what the stdint.h file would define (u)int_16 to.

              It would be odd for there to be a compiler intrinsic type to be unavailable until a header was included.

              The compiler intrinsic type could be a mess like __int16_exactly_t but without it stdint.h would have some magic line which makes a compiler intrinsic available which wasn't before, or generates a new 16-bit type ex nihilo.

              So you could have a "#pragma expose_extra_types" in stdint.h but that would not be the conventional approach.

              I mostly wanted to make the point that a mere "at least 16 bits" type is not sufficient for some use cases, which is not in response to you but the comment you responded to.

              • sparkie 1 hour ago

                Yeah, in GCC for example, we can define exact width types without including `<stdint.h>`.

                    typedef unsigned __attribute__((mode(HI))) uint16_t;
                    typedef signed __attribute__((mode(SI))) int32_t;
                

                Of course, the mode needs to be supported by the compiler for the target arch, but we don't need to include anything.

  • quelsolaar 3 hours ago

    One a modern computer, the ideal integer type is usually smaller than 64 bits. Even though the arithmetic units handle 64 bits in one op, 64 bits take up twice the memory bandwidth, and half as many fits in a cache-line, and given that a modern computer is usually bound by memory access, smaller is better. So you can argue that both new and old computers prefer 32 bit over 64, but for different reasons.

    The added compatibility between 32 and 64 bit systems, is another reason. Also if you choose int to be 64 bits, what would you make short? 16 or 32 bits? It makes much more sense to keep int 32 bits, and reserve long / long long for 64 bits.

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.

  • dnautics 3 hours ago

    That is way too much to remember

  • Joker_vD 2 hours ago

    > No, I don't believe for a second that any future platform will change their size.

    ILP64 (wherein int is 64 bits) exists. It's not very popular, but it exists; e.g. ICC supports it. So it happened in the past once already; it may again happen in the future. In any case, predicting the future is very hard, you really shouldn't be doing this.

    > IEEE memory representation of float and double

    Wait, what? I'm fairly certain that a) IEEE does not mandate the in-memory representation, and b) ARM actually uses big-endian byte order for floats/doubles when storing them in memory.

    > always behave exactly the same (leaving out strange edge cases such as denormals)

    So not always, but please pretend so? Yeah, no, thank you.

    > platforms have very long agreed on twos-complement for negative integers. This even made it into the standard at some point, I believe.

    Only in C23. It was explicitly rejected for C++ 23 (and C++ 26 too, I believe).

    > but because everyone of course did the obvious

    No, not everyone did the obvious. That's why it took so long to standardize because divergent implementations existed.

    > There are much more guarantees modern C++ code can (and should) rely on.

    As long as you only use only GCC (or Clang) exclusively, yes, you can. Otherwise, no, you can't and shan't.

    • magicalhippo 2 hours ago

      > I'm fairly certain that a) IEEE does not mandate the in-memory representation,

      That's not how I interpret section 3.2 in the standard[1]. Figure 1 seems quite explicit in how a single and a double should be encoded. The section on extended values specify they can be encoded in an implementation-depended manner, which makes the case stronger IMO.

      edit: I note that in the 2008 revision[2], it's more explicitly mentioned that the specified encoding is a binary interchange format. So that's a lot more specific than the original.

      [1]: https://pub.sergev.org/doc/ieee754-1985.pdf

      [2]: https://pub.sergev.org/doc/ieee754-2008.pdf

      • Joker_vD 26 minutes ago

        It only talks about MSBs and LSBs. It does not specify whether the LSB of the value as the whole resides in the first byte of the memory representation or in the fourth/eighth.

        And of course, if you accept the network byte order as the one intended for the interchange, then IEEE-754 mandates big-endian encoding.

        • sparkie 23 minutes ago

          Maybe you are talking past grandparent, but I think they meant "it's safe to assume float and double are IEEE-754," which is not mandated by the C standard.

    • dgrunwald 2 hours ago

      > ILP64 (wherein int is 64 bits) exists. It's not very popular, but it exists; e.g. ICC supports it.

      ILP64 is problematic for existing code: there is lots of stuff like hashcode computations using uint32_t with multiplications, relying on the C standard guaranteeing wraparound for unsigned overflows. But with 64-bit int, uint32_t will promote to a signed int, and overflows will thus be undefined behavior. This problem already exists with uint16_t multiplications on current architectures, but moving the problem to uint32_t will cause trouble for a lot of existing code that thought using fixed-size types like uint32_t would be safe.

      • nayuki 24 minutes ago

        Thank you for being one of the few people who understands that in C/C++, `unsigned OP unsigned` can have each operand be promoted to a signed integer and then have the operation overflow and cause undefined behavior.

        I chose to deal with this problem by doing a "pointless" operation to force a promotion to at least unsigned int. For example:

            uint16_t x = 0xFFFF;
            uint16_t y = 0xFFFF;
            uint16_t z = (uint16_t)((x + 0U) * y);
        

        This piece of code will work on any machine, such as: (uint16_t = unsigned short = 16 bits, uint32_t = unsigned int = 32 bits); (uint16_t = unsigned short = unsigned int = 16 bits, uint32_t = unsigned long = 32 bits).

        • Joker_vD 15 minutes ago

          But the result is 1, whether you calculate it as 16-by-16 unsigned multiplication (you get 0xFFFE0001 truncated down to 1), or 32-by-32 signed (you multiply -1 by -1 and get 1, with no overflow).

      • Joker_vD 19 minutes ago

        > stuff like hashcode computations using uint32_t with multiplications, relying on the C standard guaranteeing wraparound for unsigned overflows. But with 64-bit int, uint32_t will promote to a signed int, and overflows will thus be undefined behavior.

        Yeah, except that multiplying two 32-bit values, recast as 64-bit signed integers, will not overflow. Even adding another 32-bit value to this product will not overflow. Throw in the final cast to uint32_t to throw away the upper sign bits, and you get the identical result.

  • bobmcnamara 2 hours ago

    > All relevant platforms

    Don't make me tap the sign: the majority of processors running C are weird little dirtbag chips of 16 bits or less sprinkled by the dozen.

    • pornel 2 hours ago

      but the C for them is its own little world playing by its own rules, separate from C used everywhere else.

      And I bet that when you have only 16 bits of address space, you care how many bits every integer has.

usrnm 13 hours ago

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

  • quelsolaar 13 hours ago

    At the time its was probably very hard to know what the fixed sizes should be.

    • pjmlp 12 hours ago

      PL/I among other systems languages predating C, had the ability to explicitly define bit sizes for its types.

  • tialaramex 12 hours ago

    It turns out that you don't want integers the same size as a pointer because somebody might squirrel away capability bits in your pointer type (see CHERI) and you definitely do not want integers with capability bits.

    Rust originally says that its types usize and isize are the same size as pointers, but this was ret-conned in later Rust to say actually they're the same size as addresses for this reason.

  • sparkie 11 hours ago

    What is the size of a pointer though?

    On Intel 286 we had a 16-bit machine word and 24-bit addresses. A pointer wasn't just two machine words concatenated - the upper 8 bits were stored somewhere else - a segment register.

    On modern machines we don't (usually) need to consider this because we have a single linear virtual address space, though the size is architecture dependant - usually above 40 bits and below 64. Most common size is 48-bits, but also up to 57-bits with 5 level paging enabled.

    Either way we round up to 64-bits to store the pointer as one integer. C optionally provides types `intptr_t` and `uintptr_t`, which are integers large enough to hold the value of a pointer. Converting a pointer to `intptr_t` and back to the pointer type results in a pointer that compares equal to the original.

    However, there is no guarantee that a pointer converted to `intptr_t` and back to a pointer can be dereferenced! It works most of the time because of our linear address space and non-use of segmentation, but segmentation can still be used - the FS and GS segment registers are still available on x86_64 and are commonly used for thread local storage. If you take a `thread_local T*`, convert it to `intptr_t`, and then convert it back to a `thread_local T*` on another thread and attempt to dereference it, then despite the pointers comparing equal, they dereference to different virtual addresses.

    Integers tied to the size of a pointer would have been misguided. Pointers are not integers! (They just happen to use an integer in their representation).

    Another one, `size_t` is supposed to represent the maximum size of any object. However, that's also not well-defined. The maximum object size on the Intel 256 would have been 16-bits, because that is all you can fit in a single segment.

    On a modern machine, a `size_t` should really be 48-bits (4LP) or 57-bits (5LP), because we can't have an object larger than our maximum virtual address size - but `size_t` is typically 64-bits.

    • ykonstant 2 hours ago

      > What is the size of a pointer though?

      a MISERABLE_LITTLE_PILE_OF_BITS

    • david2ndaccount 2 hours ago

      `thread_local` is a storage class, there is no `thread_local T*` in the same way there is no `static T*`

      • sparkie 2 hours ago

        Yeah, but my point is that two pointers can compare equal but point to different addresses, thus it's not necessarily a safe operation to dereference a pointer cast from `intptr_r`.

        • david2ndaccount 35 minutes ago

          I don’t know what you mean, if you take the address of a thread local variable you get a regular pointer which is just as safe to dereference as any other pointer, cast through intprt_t or not?

          • sparkie 18 minutes ago

            It's safe to dereference in the same thread.

            A `thread_local`'s actual virtual address is not merely what the pointer contains - it's an offset from some other virtual address stored in the FS or GS register (on x86-64), which is swapped when you change thread. Casting the pointer to `intptr_t` does not retain the segment base address - only the offset. The pointer is not the absolute address.

            If you dereference in another thread, it's the same offset, but from a different base address.

            There may be other things besides segment registers on other architectures that also make it unsafe. The C standard makes no guarantee that you can safely dereference a pointer cast from `intptr_t`.

adastra22 12 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.

  • mmoll 12 hours ago

    I came here to say exactly that. There’s int_leastN_t for storage and int_fastN_t for computation. Stdint.h really gets a bad rap here.

    • lexicality 12 hours ago

      fwiw depending on use cases you might actually want to be using the fast variants for storage too, for example on arm64 you'll get aligned memory loads

spacedcowboy 11 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 3 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 11 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.

  • sparkie 11 hours ago

    > int is basically the signed version of size_t aka a word sized data type. It's not meant to have a fixed size.

    It isn't. `int` is at least as large as `short` and at least 16-bits. On modern systems `int` is still typically 32-bits whereas `size_t` is typically 64-bits. There's a `ssize_t` in POSIX for signed sizes.

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

    `long` is only at least 4 bytes, and at least as large as `int`. On MSVC (LLP64 data model) it's 4 bytes, but on SYSV (LP64 data model) it's 8 bytes. `long` should almost never be used if you actually want portable code today.

    `int` is 32-bits and `long long` is 64-bits on both LP64 and LLP64. If you want portable code using the native integer types, these are the ones you should use, definitely not `long`.

    • imtringued 10 hours ago

      Great now you made C look stupid again.

      I am not a C developer but I used to think that there was some sanity in the design. Now that residual sanity I thought was left in C has faded away. I retract all my comments in this HN submission that defend C. Whenever anyone reads them, they should consider them made erroneously in good faith by a person who wanted to justify some of the weird decisions made in C but it turns out they were just plain silly.

      Edit: actually now that I think about I'm willing to extend goodwill to C retroactively if sparkie retracts his silly nitpick and makes it charitable instead.