Sure, that's not true for 16 bit targets. But are you really going to port a 5Mb program to 16 bits? It's not worth worrying about. Your code is highly unlikely to be portable to 16 bits anyway.
The problem is with `long`, which is 32 bits on some machines and 64 bits on others. This is just madness. Fortunately, `long long` is always 64 bits, so it makes sense to just abandon `long`.
So there it is:
char - 8 bits
short - 16 bits
int - 32 bits
long long - 64 bits
Done!(Sheesh, all the endless hours wasted on the size of an `int` in C.)
I therefore use typedefs called `byte` and `ubyte` wherever the data is 8-bit but not character data. I also use the aliases `ushort`, `uint` and `ulong` to cut down on typing. On the other hand, the types in <stdint.h> are often recognised by syntax colouring in editors where user-defined types aren't.
The people who say that char is de facto signed and everyone should just deal with it, are the people who end up writing broken code.
D's `char` type is unsigned. Done. No more problems.
edit: Oh you're right
> Other models are very rare. For example, ILP64 (8/8/8: int, long, and pointer are 64-bit) only appeared in some early 64-bit Unix systems (e.g. UNICOS on Cray).
For almost every other routine task in programming, I would argue that it really doesn't matter if your int is 32 bits wide or 64 bits wide. Why go through the trouble of insisting on int32_t or int64_t? It probably doesn't matter for the things you are counting.
Some programmers will say "Well, we should use int64_t here because int32_t might overflow!" OK, so why weren't you checking for overflow if it was an expected case? int64_t might overflow too, are you checking after every operation? Probably not. "OK, let's use uint64_t then, now we get 2x as many numbers!" Now you have other overflow (and subtraction) problems to handle.
Nowadays, I just use int and move on with my life. It's one of those lessons from experience: "When I was younger, I used int and char because I didn't know any better. When I was older, I created this complex, elaborate type system because I knew better. Now that I'm wise, I just use int and char."
Minor correction, 2^32x as many numbers. Though I agree with your point.
Edit: added x to the number for consistency and clarity.
Right on, dude. I've gone full circle on that, too.
I also spent years wandering the desert being enamored with the power of the C preprocessor. Eventually, I just ripped it out as much as possible, replacing it with ordinary C code. C is actually a decent language if you eschew the damned preprocessor.
the fact that you had to have tribal knowledge about all of this is why C shouldn't stay for the long term and we should phase out languages into ones with stronger more correct defaults.
would a new programmer use "long long"? would they notice immediately that things didn't work if they didn't use it?
Rust got it correct by labeling the bits with the type directly
In the C world, only the goofballs do things like use char or int8_t for the number of children in a family, or wheels on a car.
yet that is what Rust code looks like. Almost every Rust code sample I've ever seen sets off my bozon detector just for this reason.
Now, what about SIMD types?
What about 16bits floats?
Using the short size convention we have easy and logical answers.
The reason why new languages like Rust and Zig are using those conventions is not random, types naming (and stdlib) is a weak point of C (and C++).
Luckily they are not set in stone, we can choose different and reasonable conventions.
It might be ridiculous, but it’s hardly confusing for a C programmer. But, yeah in and ideal world ‘long’ should just be defined as 64 bits
SIMD types in D are done with:
__vector(byte[16]), __vector(int[8])
and an alias (typedef for the C folk) for this is commonly used, like `byte16` and `int8`.C's "long" should not be used in new code.
The type long is the smallest ranking basic type that is at least 32 bits wide. Since int is only required to go to 32767, you use long if you need a signed type with more range than that. That made a lot of sense on platforms where int really did go up to just 32767, and long provided the 32 bit one.
Now long, while at least 32 bits, is not required to be wider than 32; if you need a signed type that goes beyond 2147483647, then long long is it.
Those are the portability rules. Unfortunately, those rules will sometimes lead you to choose types that are wider than necessary, like long when int would have worked.
Where that matters, it's best to make your code tunable with your own typedefs. I don't mean typedefs like i32 but abstract ones, like ISO C's time_t or clock_t, or POSIX's pid_t. You can adjust your types without editing numerous lines of code.
1. Consider the char and short types only if saving storage is important. Do not declare "char number_of_wheels" for a car, just because no car has anywhere near 127 wheels, unless it is really important to get it down to one byte.
2. Prefer signed types to unsigned types, when saving storage is not important. Unsigned types bend the rules of arithmetic around zero, and mixtures of signed and unsigned arithmetic add complexity and pitfalls. Do use unsigned for bitmasks and bitfields.
3. Two's complement is ubiquitous: feel free to assume that signed char gives you -128, and short gives you -32768, etc. ISO C now requires two's complement.
3. Use the lowest ranking type whose range is adequate, in light of the above rules: rule out the chars and shorts, and unsigned types, unless saving space or working with bits.
For instance, for a value that ranges from 0 to 65535, we would choose int. If it were important to save storage, then unsigned short.
The ISO C minimum required ranges are:
char 0..255, if unsigned; -128..127 if unsigned, therefore: 0..127
signed char -128..127
unsigned char 0..255
short -32768..32767
unsigned short 0..65535
int -32768..32767
unsigned int 0..65535
long -2147483648..2147483647
unsigned long 0..4294967295
long long 9223372036854775808..9223372036854775807
unsigned long long 0..18446744073709551615
If you're working with bitfields, and saving storage isn't important, start with unsigned int, and pick the type that holds all the bits required. For arrays of bitfields, prefer unsigned int; it's likely to be fast on a given target. It's good to leave that configurable the program. E.g. a good "bignum" library can easily be tuned to have "limbs" of different sizes: 16, 32 or 64 bit, and mostly hides that at the API level.If you're working with a numeric quantity, remove the unsigned types, shorts and chars, unless you need to save storage (and don't need negative values). Then pick the lowest ranking one that fits.
E.g. if saving storage, and don't need negative values, search in this order: char, signed char, unsigned char, short, unsigned short, long, unsigned long, long long, unsigned long long.
If saving storage, and negatives are required: signed char, short, int, long, long long.
If not saving storage: int, long, long long.
If the quantity is positive, and doesn't fit into long long, but does fit into unsigned long long, that's what it may have to be.
Therefore there is another use case : circular buffer indices.
a < b + c
we can safely perform this derivation (add -b to both sides): a - b < c
This is not true if a, b and c are unsigned. Or even if just one of them is, depending on which one.What I mean by "bend the rules of arithmetic" is that if we decrement from zero, we suddenly get a large value.
This is rarely what you want, except in specific circumstances, when you opt into it.
Unsigned tricks with circular buffer indices will not do the right thing unless the circular buffer is power-of-two sized.
Using masking on a poweer-of-two-sized index will work with signed, due to the way two's complement works. For instance, say we hava have [0] to [15] circular buffer. The mask is 15 / 0xF. A negative index like -2 masks to the correct value 14: -2 & 15 == 14. So if we happen to be decrementing we can do this: index = (index - 1) & MASK even if index is int.
Yes completely consistent with rules of modular arithmetic. A programmer ought to be able to extend math horizons beyond preschool. Which is ironic because I can explain this concept to my 6 year old on a clock face and it’s easy for them to grasp.
> Unsigned tricks with circular buffer indices will not do the right thing unless the circular buffer is power-of-two sized.
How will they “not do the right thing?”. With power of 2 you avoid expensive moduli operations, but nothing breaks if you choose to use a non power of 2.
> two's complement
Two’s complement is not even mandated in C. You are invoking implementation defined behavior here. Meanwhile I can just increment or decrement the unsigned value without even masking the retained value and know the result is well defined.
Like I get 2s complement is the overwhelming case, but why be difficult, why not just use the well defined existing mechanism?
And there’s no tricks here, literally just using the fucking type as it was designed and specified, why clutter things with extra masking.
There’s also the pragmatic atomicity benefit.
Non-two's complement machines are museum relics, and are no longer going to be supported by ISO C.
> why clutter things with extra masking.
Because even if the circular buffer is a power of two, its size doesn't necessarily line up with the range of a given unsigned type.
If the buffer doesn't have a width of 256, 65536, or 4294967296, then you're out of luck; you can't just uint8_t, uint16_t or uint32_t as the circular buffer index without masking to the actual power-of-two size.
(Note that uint16_t and uint8_t promote to int (on the overwhelming majority of platforms where their range fits into that type), so you don't get away from reasoning about signed arithmetic for those.)
And C++20 already standardized it I know that I already acknowledged this.
Should I go back and rewrite all the old correct code so you feel better?
(You can do that while using unsigned internally, but then you have to convert back and forth.)
The most important decision is what is the index type at the API level of the circular buffer, not what is inside it. But it's nicer if you can just use the API one inside.
The sizeof operator yielding the type size_t which is unsigned has done a lot of harm. Particularly the way it spread throughout the C library. Why do we have size_t being unsigned? Because on small systems, where we have 16 bit sizes, signed means limiting to 32767 bytes, which is a problem. In all other ways, it's a downer. Whenever you mention sizeof, you have unsigned arithmetic creeping into the calculation.
The author of the above blog article has the right idea to want a sizeof operator that yields ptrdiff_t instead of size_t. (Unfortunately, the execution is bungled; he redefined a language keyword as a macro, and on top of that didn't wrap the macro expansion in parentheses, even.)
Why so much hyperbole? You’re not out of luck. You can atomic increment/add the unsigned no matter the buffer size. You don’t worry about overflow like you would with a signed type. You can mask after.
And you continue to avoid answering the simple question: what is the advantage of the signed type. I’ve already outlined the one with unsigned, especially with atomics.
Although unsigned types have no overflow, running to them as some sort of safe refuge is a mistaken knee-jerk reaction.
In modular arithmetic, there is no such thing as <. (To put it precisely, ℤ_𝑛 is not an ordered ring.) Or are you teaching your 6-year old that 9:00 today is later than 7:00 tomorrow?
This is one of those niche uses of unsigned.
You probably want to hide it behind an API, where the domain is opaque and abstract and you have function such as a time_before(t1, t0) predicate.
True, but this is not valid if they are signed, either. Take
a = INT_MIN
b = 1
c = 2
Then a < b + c
is true. But a - b < c
invokes undefined behavior.Edit: missed
> Say that a, b and c are small integers (we don't worry about addition overflow)
Ah, well that makes this example vacuously true, however I'm not sure what the utility in that restriction is. We've only moved the goalposts from "bend[ing] the rules of arithmetic around zero" to bending the rules of arithmetic outside of "small integers".
We have moved the goalposts much farther apart.
If we are using a 32 bit integer type, all we need is that a, b and c fit into 31 bits. Then there is no way that b + c or a - b overflow. For a single addition or subtraction, we just need one bit of headroom.
I.e. the values do not actually have to be that small.
There are all kinds of situations in which programs work with small integers, where the calculations could bork if an unsigned creeps in.
A cliff near zero is qualitatively different from clipping at two extremes. An electronic device that clips everything below zero volts will distort even the faintest waveform. One that clips near the power rails has clean headroom.
If b = 0x40000000 and c = 0x40000000, b and c both fit in 31 bits, and b + c overflows to -2147483648 in signed int32 twos-complement math (I think).
Maybe the definition of "32 bit integer type" you're using is meant to encompass only 32 bits as all unsigned (but then there are a - b terms that would overflow if b > a).
Or perhaps I've gotten something else wrong.
They don't fit into a 31 bit two's complement (i.e. signed) representation, in terms of representing their interpretation as the familiar 32 bit INT_MAX.
31 bit two's complement goes from -0x40000000 to 0x3FFFFFFF. There is a 0x7FFFFFFF bit pattern, which represents -0x00000001. It has a sign bit which is 1. (So, adding that to itself does go to -2, but under that interpretation there is no overflow.)
Any pair of values in that range can be added or subtracted in 32 bit two's complement.
Including the most negative value: -0x40000000 + -0x40000000 = -0x80000000.
It's just so much less error prone to define a uint32_t. That's guaranteed to be the same
They didn't adopt it for the same reason that it is a bad idea now - too many programs already contained at least one variable named after his types.
If the standard had adopted his convention, too many programs will break, which is why his convention is currently unsuitable for any existing project.
Only ones which don't have variables named `i8` or `b32` (which is common, but not for booleans).
I've seen many projects which used the pattern [a-z][1-9]+ as variables. Those programs with a variable called `i8` won't compile if the standard made a type called `i8`.
In particular, the standard reserves entire patterns to itself, so it cannot reserve the pattern of [a-z][0-9]+. They could, and did, reserve the pattern *int*_t for themselves.
In my mind that's not a problem with the decisions taken by the author of the article, it's more of a symptom of C's limitations.
For libraries, yes, but we're talking about why the standard didn't do it.
The standard did not want[1] to reserve keywords that current programs were already using.
A library that conflicts on keywords will only break with those programs that use it. A standard that conflicts on keywords breaks all programs in that language.
> In my mind that's not a problem with the decisions taken by the author of the article, it's more of a symptom of C's limitations.
One of the constraints of taking decisions is to work within the limits existing framework - if you're avoiding the alternatives that don't break, then it's the decision-makers bug, not the frameworks.
The framework has limitations, widely published and known. You make decisions within those limitations.
[1] Although, they do do it, it's only with relectance, not on a whim to avoid typing a few characters)
What's that got to do with not breaking existing programs?
Depends. See this snippet: https://www.godbolt.org/z/5T5jz47q4
Cannot declare a variable called `u8` when there is a typedef of `u8`.
And even when you can declare a variable called (for example) `int`, that effectively "breaks" the program by not being even a tiny bit readable anymore.
But as you say, it's a personal style, and the author seems to be aware of that:
> I’m not saying everyone should write C this way, and when I contribute code to a project I follow their local style.
Because that's by far the most important rule to follow in any language.
I think the rest is less controversial, the 0 vs. NULL thing has been going on forever; I didn't check recently but I'd assume "const somestruct *foo" would still sometimes help out the compiler to optimize vs. the non-const version.
I think this is perfectly legitimate, in the same way that I don't use std libs directly but always behind wrappers or my own implementation.
The C std lib and default types are often what is keeping the language back.
And they should be used when you have no other choice.
Short name for scalar types is also pretty much the new standard for modern languages such as Zig.
Moreover, for those of us who write C fairly often, the mnemonics here are familiar.
Actually, as custom type systems go, this one is pretty elegant. Reminds me of Rust.
I gotta be honest, all of those style suggestions look good until you try them in a non-solo and non-isolated project, and then you see what a mess you created.
We've all been there, as C programmers, and we've all done that in the past, which is why we don't do it anymore
In this case it almost certainly will be - after all, the blog posts `byte` is defined as char, which could be signed or unsigned. A correct typedef for `byte` is `uint8_t`, so it's almost guaranteed that this will conflict.
Which is why I said it's best not to redefine the primitive types - you're almost certain to conflict with someone else who defined it differently.
stdint.h
It's always been amazing to me how many different projects I've worked on (not that I've been in professional C for about 7 years now)) that include their own painstaking recreation of this file.
Reusing them and effectively translating them just to your own name is just annoying to the reader IMHO. I am reminded of a C++ project I worked on, where I questioned the extensive use of typedefs around collections of things, various forms of references and compound objects etc. I was informed by one of the more experienced C++ folks that it made the code easier to comprehend.
Later I saw the typedef cheat-sheet sellotaped to the side of his monitor...
How many of them started before stdint.h existed? AFAIK, it's a somewhat recent addition to the C language, and IIRC, for a long time even after it became part of the C standard, some popular C compilers still didn't have it.
And yes, Microsoft were the outlier and absolutely dragged their heels on stdint, but you could always grab a compliant implementation from one of the FOSS projects that produced one.
I work on a born-1995 codebase. We started requiring an ISO C11 plus GNU extensions¹ several years ago and are actively removing "compatibility" checks and kludges that are outdated.
[¹ to be fair - not needing to support Windows is a godsend for any C project.]
Oh I dunno. On one hand yeah learning a quirky system is an annoyance at times. On the other hand when you're coming from a language with a real type system dealing with custom types is standard operating procedure.
I've had to patch a lot of C over the years. I can't say I've ever been bothered by types. It's always the usual suspects; hard coded offsets peppered throughout the codebase, stack smashing, baby's first callback implementation, "parsing" that omits lexing/tokenizing, archaic business logic that may-or-may not have ever been correct.
Assuming you can trust those types to be what they look like, the code is readable.
I've worked with C for well over 30 years; custom typedefs are par for the course. Work with OpenMAX libs? You have OMX_U32. On Windows? You have DWORD. Using Glib? guint32 ...
I beg to disagree. In D:
byte - 8 bits
short - 16 bits
int - 32 bits
long - 64 bits
absolutely nobody is confused about this.Lest we forget: https://web.archive.org/web/20170403130829/http://www.bobbem...
stdint already has that covered though: (u)int128_t
ubyte - 8 bits
ushort - 16 bits
uint - 32 bits
ulong - 64 bits
ucent - 128 bits
float - 32 bits
double - 64 bits
real - maximum precision hardware allows (80 bits on x87).Or, you know, we could just name them all by bit length and completely future-proof this system.
Of course, none of it worked on 32 bit machines because the programmers had never written 32 bit code before and did the portability measures all wrong.
But much C code is bringing in library headers which contain their author's own pet choices for these, which inevitably are not the same and the result is extremely confusing when you have that in play as well as the stdint.h ones.
The kernel contains a mixture of "pet" types like u32 and stdint ones, it's already confusing.
He also does make a "crazy" choice later to call his string class "s8" which clashes with his nomenclature here.
How?
But they are buggy (correct code cannot depend on the sign of `char`), which is usually the result of typedefing primitive types to save typing 3 characters on each use.