My personal C coding style as of late 2023
nullprogram.com
nullprogram.com
I'm guessing this is lacking an outer pair of parentheses (i.e. it's not `((size)sizeof(x))`) on the grounds that they're unnecessary. In terms of operator precedence, casting binds tightly, so if you write e.g. `sizeof(x) * 3`, it expands to `(size)sizeof(x) * 3`, which is equivalent to `((size)sizeof(x)) * 3`: the cast happens before the multiplication. Indeed, casting binds more tightly than anything that could appear on the right of sizeof(x) – with one exception which is completely trivial.
But just for fun, I'll point out the exception. It's this:
(size)sizeof(x)[y]
Indexing binds more tightly than casting, so the indexing happens before the cast.
In other words, it's equivalent to `(size)(sizeof(x)[y])`, not `((size)sizeof(x))[y]`.But you would never see that in a real program, since the size of something is not a pointer or array that can be indexed. Except that technically, C allows you to write integer[pointer], with the same meaning as pointer[integer]. Not that anyone ever writes code like that intentionally. But you could. And if you do, it will compile and do the wrong thing, thanks to the macro lacking the extra parentheses.
…On a more substantive note, I quite disagree with the claim that signed sizes are better. If you click through to the previous arena allocator post, the author says that unsigned sizes are a "source of defects" and in particular the code he presents would have a defect if you changed the signed types to unsigned. Which is true – but the code as presented also has a bug! Namely, it will corrupt memory if `count` is negative. You could argue that the code is correct as long as the arguments are valid, but it's very easy for overflow elsewhere in the code to make something accidentally go negative, so it's better for an allocator not to exacerbate the issue.
With unsigned integers, a negative count is not even representable, and a similar overflow elsewhere in the program would instead give you an extremely high positive count, which the code already checks for.
Personally I prefer to use unsigned integers but do as much as possible with bounds-checked wrappers that abort on overflow. Rarely does the performance difference actually matter.
This particular part is not actually complicated: the postfix operators bind the most tightly, then the prefix ones, then the infix ones. (The last part is quite messy, though.)
So (int)x[y] parses the same way as, for example, *p++, which should be familliar to a C programmer.
> I could use _Bool, but I’d rather stick to a natural word size and stay away from its weird semantics.
This is even more subjective, but personally I like _Bool's semantics. They mean that if an expression works in an `if` statement:
if (flags & FLAG_ALLOCATED)
then you can extract that same expression into a boolean variable: _Bool need_free = flags & FLAG_ALLOCATED;
The issue is that `flags & FLAG_ALLOCATED` doesn't equal '0 if unset, 1 if set', but '0 if unset, some arbitrary nonzero value if set'. (Specifically it equals FLAG_ALLOCATED if set, which might be 1 by coincidence, but usually isn't.) This kind of punning is fine in an `if` statement, since any nonzero value will make the check pass. And it's fine as written with `_Bool`, since any nonzero integer will be converted to 1 when the expression is implicitly converted to `_Bool`. But if you replace `_Bool` with `int`, then this neither-0-nor-1 value will just stick around in the variable. Which can cause strange consequences. It means that if (need_free)
will pass, but if (need_free == true)
will fail. And if you have another pseudo-bool, then if (need_free == some_other_bool)
might fail even if both variables are considered 'true' (i.e. nonzero), if they happen to have different values._Bool solves this problem. Admittedly, the implicitness has downsides. If you're refactoring the code and you decide you don't really need a separate variable, you might try to replace all uses of `need_free` with its definition, not realizing that the implicit conversion to _Bool was doing useful work. So you might end up with incorrect code like:
if ((flags & FLAG_ALLOCATED) == true)
Also, if you are reading a struct from disk or otherwise stuffing it with arbitrary bytes, and the struct has a _Bool, then you risk undefined behavior if the corresponding byte becomes something other than 0 or 1 – because the compiler assumes that the implicit conversion to 0 or 1 has been done already.if (need_free == true)
Is such a horrible code smell to me. You have a perfectly good boolean. Why compare it to a second boolean to get a third boolean?
if (need_free)
or
if (!need_free)
for the opposite case is so much better.
I will admit that in my world this leaves
if (need_free == some_other_bool)
as something I don't have a particularly comfortable way of doing safely.
#define FLAG_63 (1ULL << 63)
long long flags = FLAG_63;
In this case, if (flags & FLAG_63) pass();
will pass, but typedef int BOOL;
BOOL set = flags & FLAG_63;
if (set) pass();
won't pass, due to truncation.Question: Would you argue that a datatype that holds the smallest (1-bit) datum should be as wide as the largest integer type just to handle such cases?
If so, that would be highly inefficient for storage purposes. Note that Win32 has 32-bit BOOL type, but internally NT uses 8-bit BOOLEAN type to store bools in structures.
> if (need_free == true)
> Is such a horrible code smell to me. You have a perfectly good boolean. Why compare it to a second boolean to get a third boolean?
> if (need_free)
You are probably interested if the `need_free` flag is set to true, and not if `need_free`. It is true that `if (need_free)` has the same behaviour, but it is some steps farther from what you are interested in."You are probably interested in whether it's true that the 'need_free' flag is set to true"
leading to
> if ((need_free == true) == true)
? Answer: because that extra layer of indirection adds nothing, and just gives you a bit of extra cognitive load and an extra opportunity to make mistakes. I think the same is true about going from "need_free" to "need_free is set to true".
(This becomes less clear if you have variable names like 'need_free_flag'. I say: so don't do that then! It's almost always appropriate to give boolean values and functions that return boolean values names that reflect what it means when the value is true.)
if (need_free == some_other_bool)
you could use: if (!need_free ^ some_other_bool)
if you're using _Bool.Actually, and this is probably surprising to many, this is equivalent to
(size)(sizeof ((x)[y]))
sizeof is not a function but a unary operator, and indexing (as well as function calling...) binds stronger than the sizeof operator. It is not a function, not even syntactically! Hence why I strongly prefer putting a space after the sizeof keyword, and to not use parens for the operand unless needed.https://en.cppreference.com/w/c/language/operator_precedence
So the "correct" way to define the macro is
#define sizeof(x) ((size)(sizeof (x)))
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.
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.
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.
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
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.]
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 ...
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.
And manually writing out Win32 API prototypes instead of including windows.h might shave off some compile time, but it's like ignoring a well-maintained highway to trek through the woods. Just seems like a lot of these changes are about personal preference rather than sticking to what makes C code easy for everyone to work with.
Where it does go to pieces is when two different programs both define u16, use them in header files, and then a third program tries to include both those header files at the same time. The big advantage of <stdint.h> is avoiding that failure mode.
The namespaced library type equivalent is something like libname_u32, at which point it's tempting to write uint32_t instead of the libname:: or libname_ prefix.
It is not about saving keystrokes, it is about reducing sensory load when reading it.
Sorry, I know it may sound like I'm splitting hairs, but every single time when the argument of verbosity vs conciseness in programing languages comes around, this "keystrokes" argument is thrown and it is extremely flawed. The core belief that conciseness is only better for faster typing but that verbosity is somehow always better than conciseness for reading is just plain wrong and we should stop using it. And yes, verbosity has some advantages for reading comprehension, but so does conciseness, no side is a clear winner, it is all about the different compromises.
This seems a theoretical possibility _at best_, and a fairly strawman-y one at that. I doubt any competent C programmer would get "confused". Irritated at a different style, maybe.
Too, everything is hard to read before you learn to read it. -- Rich Hickey
Maybe I'm a beginner then. He lists a few cases where it's not worse than sticking to 8-bit bools, but no cases where it's actually an improvement. It still wastes memory sometimes, e.g. if you have adjacent booleans in a struct, or boolean variables in a function that spill out of registers onto the stack. Sure it's only a few bytes here and there, but why pessimize? What do you gain from using a larger size?
Architectures are generally optimized for aligned access (or disallow unaligned access), but what counts as "aligned" is different for each type.
A char type that is used for a bool can be accessed on any byte boundary because the alignment of a char is 1. The alignment of a 32-bit value is 4.
However, architectures are generally more optimized for 32-bit operations in registers. If you're dealing with a char in a register, the compiler will generally treat it as a 32-bit value, clearing the top bits. (This is one of those places where C's UB can bite you.)
However, there are architectures where 32-bit access is optimized.
My specific bug bear here was a junior who insisted "saving space" by packing the structs and using a single 8 bit byte for the conditionals.
Their 'improved' code ground throughput on intel chips by a factor of 10 or so and generated BUS ERRORs on SPARC RISC architectures.
By packing the header of the structs they misaligned the array of data values such that the intel chips were silently fetching two 32 bit words (say) to get half a word from each to splice together to form a 32 bit data value (that was passed straddling a word boundary) to pipe into the ALU and then do something similar to repack on the other end - SPARC's quite sensibly were throwing a fit at non aligned data.
Point being - sometimes it makes sense to fit data to the architecture and not pack data to "save" space (this is all for throughput piped calculations not long term file storage in any case)
A what now?
Though I'm entirely sure not when something is considered private or public. You can obviously make changes to a GPL repo, compile it and run the executable yourself and just never release the source code.
But what happens when you start sharing the executable with your friends, or confine it to a company?
"I made this GCC fork with some awesome features. You can contact me at joe@gmail.com if you're intere$ted ;)"
Of course, for almost all practical cases, the source code for a GPLed program is made available as a download off the Internet because the mail order disc route seems really archaic these days and probably would be removed altogether in a GPL version 4 if some prominent company used this loophole to evade the spirit of the GPL. Either that or somebody would jump through your hoops to get the source and just stick it on a public GitHub repo. If you then DMCA that repo, you'd be in violation of the GPL.
If you share an GPLed executable with your friends or with other people at a company, then they'd presumably be able to request the source code. But if you run a Cloud GCC service with your fork, you could get away with keeping your source code proprietary because GCC isn't under AGPL.
As far as the licensing part goes they give you the source code, but last time I tried I could not get it to compile. Kind of lame and sketchy in my opinion.
[1] https://www.microchip.com/en-us/tools-resources/develop/mpla...
> Maybe I'm a beginner then. He lists a few cases where it's not worse than sticking to 8-bit bools, but no cases where it's actually an improvement. It still wastes memory sometimes
They key part of my response is sometimes "wasting memory" (to gain alignment) is a good thing.
If someone, a beginner, is concerned about percieved wasted memory then of course they will use "packed".
As for the guts of your comment, I agree with your sentiment but would exercise caution about expecting a compiler to do what you expect in practice - especially for cross architectural projects that are intended to be robust for a decade and more - code will be put through muliple compilers across multiple architectures and potentially many many flags will be appied that may conflict in unforseen ways with each other.
In general I supported the notion of sanity check routines that double check assumptions at runtime, if you want data aligned, require data to be big endian or small endian etc then have some runtime sanity checks that can verify this for specific executables on the target platform
Nuance is that each field should be at an address divisible by the fields size or wordline size, not some magic 32 constant. The entire struct should also be padded to a multiple of the largest fields size. In practice this usually means 32 bit alignment.
Which almost noone ever does. It's very hard and almost never has any benefit. At that point you have way different problems than programming style choices...
Otherwise there's thread_local mylibrary_errno, which might actually be the right thing for within a library, translating it to an enum return on the boundaries.
if(foo(x,y, out1, out2) != WHATEVER_LIBRARY_OK) { ... }Well, I should probably just say "We're done here." and stop reading the rest of the article. "Signed sizes" are an extremely surprising abstraction break that are just asking for disaster.
> No const. It serves no practical role in optimization, and I cannot recall an instance where it caught, or would have caught, a mistake.
Should you even be writing C if you haven't hit this? People mix up "in buffers" and "out buffers" all the time. "const" flags this immediately.
> Declare all functions static except for entry points. Again, with everything compiled as a single translation unit there’s no reason to do otherwise.
And when you go trying to debug something and get at a variable or function that you can't find because everything is "static", you'll curse the one who wrote the code.
> Another change has been preferring structure returns instead of out parameters.
Which is a great way to accidentally return a pointer to your stack and open a big ass security hole. Passing in the output buffers makes clear the ownership semantics.
This guy seems like he mostly writes code for 64-bit systems. The coding advice is ... okay, I guess? Maybe? In that domain?
In a 32-bit embedded domain, some of these guidelines are a good way to get youself into a lot of trouble in a real hurry.
(The static thing might depend on the tooling. I went static-by-default about 15 years ago, around the same time I went full size_t, and I've yet to have a problem with it.)
Codebases have their own conventions and design patterns. If you have that const is a needless formality.
Code should be being simple and clean first, constantly stating things that are obvious 90% of the time isn’t that.
Bjarne Stroustrup wrote a detailed memo advocating for signed sizes:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
Modern C/C++ compilers can and will warn you (quite aggressively) if you mix signed and unsigned numbers without thinking about it.
A lot of the examples also seem weird. Eg, he gives a negative example of a function:
unsigned area(unsigned x, unsigned y) { return x * y; }
In this, he complains that you can still write buggy code: area(height1-height2, length1-length2);
He's right - that is potentially buggy, But, that code would be buggy whether the area function took signed or unsigned numbers as input. However, the signed version of this function is still worse imo because it could hide the logic bug for longer. If the area function should always return a positive number, I'd much rather that invalid input results in an area number like 4294967250 than a small negative number.Similarly, accidentally passing a negative index to a vec is much more dangerous with signed indexes because v[-2] will probably quietly work (but corrupt memory). However, v[4294967294] will segfault on the problematic line of code. That'll be much easier to find & debug.
And a lot of the examples he gives, you'd get nice clear compiler warnings in most modern compilers if you use unsigned integers. You won't get any warnings with signed integers. Your program will just misbehave. And thats much worse. I'd rather an easy to find bug than a hard to find bug any day of the week.
z=x+y; if(z < x || z < y) // overflow
And bounds checks are just a single comparisons against an upper bound (handles both over and underflow) size = x + y;
// or
size = x - y
if(size < bound) // good to go
Prior to C23 (stdckdint.h) its very error prone to check for signed overflow since you have to rearrange equations to make sure no operation could ever possibly overflow.https://dlang.org/blog/2023/10/02/crafting-self-evident-code...
(The article is crafted around D, but the principles apply to C as well.)
What happened to the conditional expressions? Move them to the interiors of doX() and doZ().
That was an interesting point. Not sure that it's always valid but I guess it depends where you want the abstraction to lay, and how it affects the mental construct around the code.e.g.
deleteRecords();
is not better than if let x = deadRecords()
deleteRecords(x);
Sure, it looks messier but there is value is showing upfront that you're pruning and not wiping.If the author wisely renames his function e.g. pruneDeadProjects(), yes. But merely moving the the condition within the function can be dangerous for context and be a leaky abstraction.
Like your idea of pruneDeadProjects()!
I'm not sold on the structs as return types thing. I prefer just a numeric error code as a return value, and out parameters for any other returns.
I think this is more-or-less a C/POSIX standard convention. E.g., `pthread_t` vs. `struct stat`.
Totally agree, I even wrote this as a blog post: https://www.lelanthran.com/chap9/content.html
The pattern I like to use for this is to expose class definitions, and declare each field with an underscore suffix to indicate it's private.
typedef struct Foo {
int x_; // implementation detail. don't touch.
} Foo;
Another pattern I've seen for this is to use truly opaque structs of byte arrays, that are then typecast into the proper struct in the implementation. // foo.h
#define FOO_SIZE_ 4
#define FOO_ALIGN_ 4
typedef struct Foo {
_Alignas(FOO_ALIGN_) char impl_[FOO_SIZE_];
} Foo;
// foo.c
#include "foo.h"
typedef struct FooImpl {
int x;
} FooImpl;
_Static_assert(FOO_SIZE_ == sizeof(FooImpl), "");
_Static_assert(FOO_ALIGN_ == _Alignof(FooImpl), "");
void Foo_bar(Foo const* self_opaque) {
FooImpl const* self = (FooImpl)self_opaque;
...
}
I'm not a big fan of this approach, but it does protect your clients against themselves.I agree with this. One of the things I dislike about SDL_net, etc, is they do exactly what you're describing. It's a pointer but they typedef it as if it's a value type.
I understand the intent but imo that's very icky.
I’ve started writing a bare metal OS for Arm64. It’s very early but I’ve done some similar things. I’m using pascal strings, I’ve also renamed the types (though I’m using “int8” style, not “i8”).
I quickly decided that I never intend to port real software to it, so I really don’t have to conform to standard C library functions or conventions. That’s given me more freedom to play around. C is old enough to have a lot of baggage from when every byte was precious, even in function names.
It’s nice to get away from that. Much like the contents of this post, that plus other small renamed just ended up feeling like a nice cleanup.
[1] https://github.com/libjpeg-turbo/libjpeg-turbo/commit/52ded8...
So you're just building it as just a hobby, won’t be big and professional like gnu?
> typedef double f64;
Assuming float is 32 bits and double is 64 bits sounds like a foot-gun. OpenCV defines a float16_t [0], CUDA implements half-precision floats [1], micro-controllers implement whatever they want.
C++23 introduces fixed width floating-point types [2], but not aware of any way to enforce this in C. What I would suggest it to have a macro to check data is not lost at compile time.
Generally I agree with others, it might be better to leave some of these things as default for readability, even if it is not concise.
[0] https://docs.opencv.org/4.x/df/dc9/classcv_1_1float16__t.htm...
[1] https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH...
typedef _Float32 f32;
typedef _Float64 f64;
https://gcc.gnu.org/onlinedocs/gcc/Floating-Types.htmlEven Linux started as a personal project, but because of its quality and the need it met it quickly spread. So please write your code in such a way that experienced other C programmers can read it easily.
In isolation, I like some of his ideas, but some issues with C remain, and he is perhaps just to comfortable with C to jump ship and embrace Rust, which has many things he likes and more (e.g. no buffer overflows by design).
I adopted the style of writing all macros in lower case with the prefix "macro_", so i can grep through all macros. So macro_ becomes like a keyword. Same with enum.
I use almost the same naming scheme for i32,f32, etc., but i typedef for example size_t const to usz, and size_t to usz_ (or use macro mut(x) x ## _) . So all shorter type names are const by default. I use a single header of 35 sloc to do that.
I see that this way of coding can be confusing for other people, so i avoid it when writing code that other people have to work with. But for personal code its really enjoyable for me.
That breaks any macro that uses sizeof in its expansion, and subtly changes any code snippet you might bring into the code that uses sizeof, even if those macro are defined first.
Speaking of which, if you define a macro for a C keyword before including any standard header, the behavior is undefined.
It's an unparenthesized unary expression, which has a lower precedence than postfix. sizeof(x)[ptr] will turn into (size)sizeof(x)[ptr] which parses as (size) ( sizeof(x)[ptr] ).
This reminds me of "#define max ..." in Windows.h. Not as bad, but if you autoreplace `sizeof(ptrdiff_t)` with `sizeof(size)`, good luck, because it will output size of type of size variable, if it exists in the scope.
No const.
Please don't. `const` is incredibly valuable, not only to the reader, but to the compiler.
Take for example:
int Foo_bar(Foo const* self);
Just looking at this signature, I know that calling `bar()` will not modify the state of the object. This is incredibly valuable information to the reader.Furthermore, if I want to create a `Foo` constant, I can only call this function if it is `const`.
static Foo const a_foo = FOO_INIT(&some_params);
return Foo_bar(&a_foo); // Will not compile without 'const' in function
`const` is valuable to the compiler, since `a_foo` can be placed into ROM on some platforms like MCUs, saving precious RAM.That I can agree with TFA. However I agree with the GP that dismissing it entirely is a little misplaced. It serves as a hint/documentation and I think the article undersells the value of rodata (not the pointer use of const which is basically shit).
I mean I have seen at least a few SIGSEGV/aborts due to attempted writes to ro memory. Also like, one of the few modern justifications for C, embedded, const still has important link time meaning.
The consequence is that you may define two classes, one non-mutable and one mutable like String/StringBuilder.
As a side note, I would say the interface is unmodifiable, not immutable, because references of the interface type may refer to mutable instances that can mutate while you use it through the interface. Immutable = doesn’t change state, unmodifiable = you can’t change it’s state via that reference (but it might change it’s state due to other concurrent code holding a mutable reference). This nomenclature comes from the “unmodifiable” collection wrappers in Java, which don’t make the underlying object immutable.
static + const is valuable, but const parameters are merely a convention, there is no actual enforcement around them and due to aliasing the compiler generally can’t assume the parameter doesn’t actually change anyway.
No, but it can warn you!
The type is meant to capture programmer intention, and if you use `const` the compiler can warn you that your intention does not match the intention of the existing code (like, the intention of the author who wrote Foo_Bar).
int Foo_bar(const Foo * self);Not to be confused with `Foo *const`
I didn't say this. I said a `const` function tells the reader that the state of an object doesn't change.
Another reader correctly pointed out that there are ways to modify the state of a `const` parameters (indirection and const cast), but I would argue that such an API is poorly-designed.
To qualify my original comment, a reader only knows a function doesn't change an object's state if the API is well-designed.
let x = foo()
... to ...
const x = foo()
...runs foo at compile time to get the value. I dunno I just thought it was neat.
Consistency, conciseness and clarity (you don't have to guess much about those, once you've understood the naming scheme)
My brain wants to read f32x4 as a 32 by 4 matrix of floats.
(That being said I'd definitely be interested in trying your convention in the context of something like Rust.)
And when working on some heavily optimized SIMD code on the CPU side, I tend to use the default types even less.
IMO including the number of bits in all primitive types is usually an overcorrection from trauma caused by C/C++'s historic loose definitions of primitive type sizes. However I don't write much CPU SIMD code and can definitely see how you'd develop your preference from that context.
Pointer-to-const on the other hand (as in "const Foo *x") is a bit of a fluff and it spreads like cancer. I agree with the author that const is a waste of time. And it breaks in situations like showcased by strstr().
I use pointer-to-const in function parameter lists though (most of the time it does not actually break like in strstr()): as documentation, and to be compatible with code that zealously attaches const everywhere where there (currently) is no need to mutate.
But overall my use of const is very very little and I generally do not waste my time (anymore) with it. I almost never have to use "const casts" so I suppose I can manage to keep it in check. In C++ it is a bit worse, when implementing interfaces, like const_iterator etc. That requires annotating constness much more religiously, and that can lead to quite a bit of cruft and repetition.
const is indeed viral when eg, used in apis, but it's a strong indication at a glance for api users what they can expect to happen to the memory the pointer points to, whether it's just for input or is modified... and the virality is only a pain (it can be a pain) if you didn't use it from the start so all the things it might call are already kitted out with it.
However, when used for members in datastructures, it's more often than not problematic.
This is often not true, and even if you think so, you're often wrong. I can't recall all the consequences of the flaws in the system (promote/demote const), but it's not fun to deal with.
I've seen so many things wind up passed to a function or going through an interface eventually that's non-const (or lets not forget is "const'd for safety").
This is where some would say you should give up on practical grounds... if the mission is to determine which const scenarios can be ensured, you argue this is not practically possible and throw the whole thing out.
There's a really good chance you will either have to promote or cast away the const, which I hate.
I tend to agree regarding not using const. It's been a while, so I don't have an example off the top of my head, but it's incredibly easy to break the const mechanism and have to deal with these annoying flaws.
I've just seen this go really bad with any kind of code that has a split responsibility between teams. Eventually you will have to pass to a non-const interface, that you aren't supposed to change.
So perhaps it makes sense if you have control from the top down and can ensure that the constness is maintained, or completely not, if it ends up non-const (then you could also try to move the interface to const, if it truly is)...
... I also suspect in many projects you'd just have to come to the conclusion that nothing can be const'd, because it ends up non-const anyway. Thus leading to the conclusion "just don't use const".
P.S. I'm a bad boy that didn't read TA yet. This is just based on my past experience where we didn't really have the authority to change stuff in the stack... often times there was eg an MCU interface at the end that was non-const... guess we could contact the silica manufacturer... sure they'll get right on that.
The strstr() signature is probably the shortest example / explanation why. To implement strstr(), you have to hack the const away to create the return value. Alternatively, create a mutable_strstr() variant that does the exact same thing. This is the kind of boilerplate that we don't want in C (and that C is bad at generating automatically).
Think about it this way: Real const data doesn't exist. It always gets created (written) somewhere, and usually removed later. One way where this works cleanly is where the data is created at compile time, so the data can be "truly" const, and be put in .ro section, and automatically destroyed when the process terminates. But often, we have situations where some part of the code needs to mutate the data that is only consumed as read only by other parts of the code. One man's const data is another man's mutable data.
In C, the support for making this transition work fluently is just very limited (but I think it's not great in most other languages, either).
Ever seen a ROM?
And the C library‘s hacks around not being able to overload functions (which is the only reason for strstr et al‘s weird signature) wouldn‘t stop me from using const. It can be really useful both for documentation and for correctness. Think memcpy, not strstr.
How does the data get onto the ROM?
But read 2 sentences further, where I had addressed this already.
> Think memcpy, not strstr.
See my other comments, I do think that making const function parameters is generally good for documentation and compatibility. strstr() is only a showcase for the limitations. Typically, const works for function parameters but not data structures.
It seems to me the "hacking" is exactly the side-effect that is wanted. It's like the requirement in Rust to do certain kinds of things in an `unsafe { }` block (or using the `unsafe` package in Go): not that you want the compiler to prevent you from doing things completely, but that you want the compiler to prevent you from doing things by accident.
> One man's const data is another man's mutable data.
Yes; and the point of `const` for function parameters is to make sure that data isn't mutated unexpectedly.
It is not. It's broken at the surface level. If you passed a pointer that is already const on your side, you get back a non-const pointer back that allows you to write to your const memory.
This introduces flaws in the type system. I wish I had a better breakdown on the impact of these concerns, but I'd rather not worry at all.
Anyway, if you don't use const, this goes away. Bear in mind the minor amount of "safety" it provides, because you can just ignore it later, as you arguably tend to be doing anyway when you pass a const to non-const or visa versa.
Inevitably, outside of really small insular project (and often times even then), there's something down the line that winds up being non-const that you don't want to change.
C developers of this mindset tend to just come to the conclusion that you will immediately break the type system, just give up on the whole game.
Edit adding at least on example:
Example: You define as const and remove the const later. If anything writes to the non-const, this is undefined behavior
Example: I believe the above is actually true for const promotion if you modify the non-const version... I think this is only after the call (edit. ie after it become const, really interest in the answer).
No Undefined behavior
/* I imagine this would be okay */
si_non_const = si_non_const + GetMagicValue();
/* Const is promoted here */
const int fparam = si_non_const;
/* Writing to fparam is undefined past here */
f_const(&fparam);
Undefined behavior
/* I imagine writing, after using as const is also not defined, but is fine at this point */
si_non_const = si_non_const + GetMagicValue();
/* Here we now have a constant value that will never be written to */ const int fparam = si_non_const;
f_const(&fparam);
/* I think this would also be UB, even though it's accessed through a different symbol */
si_non_const = si_non_const + GetMagicValue();
Interested in other opinion, maybe will think on later... would it be valid for the compiler to remove that last assignment?
Edit: Sorry, this is unreadable, if you put a space between the not undefined, and undefined it's easier
I only agree with "Declare all functions static except for entry points".
s8(s) is only for literal strings, it should be called s8_c instead and keep s8 for the default ctor.
The struct return part is okay, but I"ve never used. This is not Common Lisp.
If you're writing code that never gets reused, then it's fine, no need for static functions.
Technically, it's illegal to #define over a language keyword.
> #define int long
Because you're replacing the int keyword with something else.
The standard says:
> 17.6.4.3.1 [macro.names] paragraph 2: A translation unit shall not #define or #undef names lexically identical to keywords, to the identifiers listed in Table 3, or to the attribute-tokens described in 7.6.
> 7.1.2 "Standard headers" §5 [...] The program shall not have any macros with names lexically identical to keywords currently defined prior to the inclusion of the header or when any macro defined in the header is expanded.
In C standard, it is not allowed.
> C11, § 6.4.1 Keywords
> [...]
> The above tokens (case sensitive) are reserved (in translation phases 7 and 8) for use as keywords, and shall not be used otherwise.
https://stackoverflow.com/questions/12286691/keywords-redefi...
Don't let attackers influence the size of a buffer (neither for VLAs nor for heap allocations).
The general rule for sizeof is to apply it only to variable names or directly to typenames.
I have seen many people redefine 'for' and 'while'. These people often argue that it is an improvement.
cpp
#define sizeof(x) (size)sizeof(x)
sizeof(UU)
^D # 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "<stdin>"
(size)sizeof(UU) #define while ifI've always felt that C is unfairly maligned. Yes, it's very low level, it's meant to be. Yes, it lets you shoot yourself in the foot, but what language doesn't?
Most of the problems with C are really issues with the standard library, the Unix (now Posix) interfaces, and the string type.
None of these are actually part of C, but are part of how C is normally used. So those problems can be avoided, and use C for what it's good at.
Isn’t it a beauty of lower level languages that creating higher level abstractions provides more value?
edit: typo
Good lord.
In higher level languages, you can't shoot yourself in the foot nearly as easily in such a way as to trivially create a correctness problem and security vulnerability (like a buffer under/overflow). Languages like Java and C# make it pretty difficult to shoot yourself in the foot this way (though you still can in other ways, like with incorrect concurrency). Rust makes it a lot harder to shoot yourself in the foot across the board, especially on accident (i.e., without being aware that you're something dangerous and low-level, viz. `unsafe`).
Jokes aside though, I doubt that no programmers are immersed in the computer while they code. It totally is a different world, just implemented inside this one.
Then they picked up a JS or Python class, were told high-level languages are easy and viola! they started to understand programming.
That's the reason people are spiteful of it. They had a terrible learning experience right out the gate.
Usually folks attach a debugger to capture a stack trace. Usually the debugger uses debug info to determine where the program is, and it's stack trace. Or it can walk frame pointers. Depends on if either are even used, which is a compile time decision.
But I don't like using 1 and 0 instead of booleans. Many standard C functions (fclose for example), return 0 on success. Better to be explicit here.
Some of my own style changes this year:
I try really hard to write functional code. Mainly try to keep functions pure, and write declarative code. I find that this makes the code easier to write(not necessarily read), and I'm less scared of bugs.
I also avoid malloc unless I absolutely need it. You can usually preallocate space on the stack or use a fixed length buffer, which pretty much avoids all fears of memory leaks or use after free type bugs. You will sometimes waste memory by allocating more than you need, but it's a lot more predictable.
This seems like a bad idea, because the whole point of an assert is that something shouldn't happen, but might due to a (future?) bug.
And so it’s a bad idea because…?
The whole idea is to notice a bug before it ships. Asserts are usually enabled in test and debug builds. So having an assert hit the “unreachable” path should be a good way to notice “hey, you’ve achieved the unexpected” in a bad way. You’re going to need to clarify in more detail why you think that’s a bad thing. I’m guessing because you would prefer this to be a real runtime check in non debug builds?
The nice thing about this approach is that the assertion provides value both in debug and release mode. In debug mode, it checks your invariants. And in release mode, it makes your program smaller and faster.
Personally I quite like rust's choice to have a pair of assert functions: assert!() and debug_assert!(). The standard assert function still does its check in both debug and release mode. And honestly thats a fine default these days. Sure, it makes the binary slightly bigger and the program slightly slower, but on modern computers it usually doesn't matter. And when it does matter (like your assertion check is expensive), we have debug_assert instead.
Usually in release mode you want to log the core dump and then fix the bug.
The unreachable pragma suggested by the author is just a more extreme version of the latter choice.
> #define lengthof(s) (countof(s) - 1)
It makes no sense to use the word "length" to mean one less than the number of items. You could call it maxindexof perhaps.
There may be good arguments for zero based indexing, but we have to also accept that there are downsides. One is that your code has to feature an artificial quantity obtained by subtracting one from a meaningful quantity.
If you pack too much information, you are taxing your brain more, you are slower to analyse the code and you make it easier to make mistakes.
Now I much prefer code that is as verbose as possible.
I like petunias! Now what? How does that help anyone?
I'm certain your opinion on petunias and your possible distaste for orchids will be welcomed in a flower-news type orange site. :-)
"ALL_CAPS" in C was not for constants, but for preprocessor macros. It's shouting in all-caps, because it means "Look out! There's a cpp macro expansion here!"
Related, please stop using "ALL_CAPS" for constants in other languages. Not only does shouting constants as the most prominent syntax in the code make no sense, but there are much better uses for shouting in a programming language.
(For an example of a good use of "ALL_CAPS": if your language ever acquires Scheme-like template-based hygienic macro transformers, "ALL_CAPS" (or "ALL-CAPS") is excellent for making template pattern variables stand out within the otherwise literal code blocks.)
Changing this would be a huge undertake I'd be afraid of engaging, if I cared that much.
Constants are so innocent and useful. Why indirectly discourage their use by making their usage an eye-bleed?
static constexpr int kConstantName = 42;When the type is already "constexpr int", that k isn't helping.
Sometimes when I use C++, I will namespace them however:
namespace constants {
static constexpr int name = 42;
}
…
int x = constants::name;
The exact name of the namespace would depend on the use and could be something more descriptive. do_thing(foo); // foo is variable
do_thing(FOO); // FOO is constant (i.e this call should always do the same thing)
foo = FOO; // I wanna name a variable the same as a constant do_thing(Constants.foo)
as equally clear...I like this as a convention, but not necessarily as a grammar rule. Printing values is common enough that it shouldn't require shouting for constant attention, simple code shouldn't trigger sensory overload.
#define DECLARE_STRING(variable_name, string) struct{size_t allocated; size_t used; char string[sizeof(c_string)];} variable_name ## internal = {.allocated = sizeof(c_string) - 1, .used = sizeof(c_string) - 1, .string = c_string}; MyString *variable_name = &variable_name ## internal
Its a lot of C99 magic, so it may not be what you want but it is possible.
The primitive type names should be native, but to "fix" C I prefer using the C preprocessor (I use it for namespace/name mangling too). This is not perfect, but should be already way more than enough.
With proper preprocessor usage (without going amok), one can write one compilation unit software roughly easily.
it seems this may be more an issue with being shy, than a coding issue :P
in my view, language is a Style, so coding in C is c-style coding.
when i started coding in python (small projects initially), my code cried-out Java Java (typing, packaging, naming, oop), and took nearly as long to write - I laughed my head of when I realized, just because I could doesn't mean I should.
Correct, the standard does not specify whether char is signed or unsigned, so it's implementation-specific.
> and may be more or less than 1 byte, right?
Wrong, char is specified as a single byte character, so the following will always be true:
sizeof(char) == 1;I stopped reading there. I wish this guy a happy coding (and non-coding) life, but I hope we never work together.
I don't agree with all stylistic choices in his code, but the level of experience and skills are far above most C developers.
U1, U2, U4, U8, I1, I2, etc
Also S for "slot" aka unsigned pointer sized integer (usize_t)
Another big point is formatting code to line-up instead of with an autoformatter. When you are doing something which is almost the same but slightly different it helps readability considerably. It is also a sign of a well-loved codebase, since I've never seen an autoformatter that can do it.
Maybe we could make formatters at least auto _detect_ that code is already aligned and to just leave that code alone. Some kind of "love heuristic"
Most of the time when I use fixed-width int types I’m trying to create guarantees for bitwise operators. From my perspective I feel like it therefore makes the most sense to name types on a per-bit level.
I also like that it makes all the type names the same width (notably U1/U2 vs u8/u16).
- I prefer functions over classes.
- no mangling of exported names, the binary is re-usable as API.
- in the long term, the C source code is more re-usable in other projects than C++ ones.
and more like this.
ps. you know you can write C++ code that is functional and use free functions primarily instead of putting everything in classes?
And once you fix it, you have built a light weight library that you can use from any other language.
Also these pointer manipulation is what gives C its power.
Then use functions?
> no mangling of exported names, the binary is re-usable as API.
Not once in my life have I seen someone use a binary as an API.
> in the long term, the C source code is more re-usable in other projects than C++ ones.
I don't even know what you mean by that.
He means that if you write your library in C it is callable from Python, Java, C++, Ruby, PHP, Python, Perl and more.
Because C++ without exceptions is not C++, and those languages cannot catch exceptions, nor call overloaded functions, nor delete or create objects using new and delete, nor refer to fields with classes, nor call methods on objects.
Write exception-free external APIs. It's not that hard.
> call overloaded functions
Write an API that doesn't overload functions?
> nor delete or create objects using new and delete
Write an API around that. You'd need to do it in C anyway.
> nor refer to fields with classes
What?
> call methods on objects
If you can call a function, you can call a method.
Your complaint is basically "you cannot write C++ in Python". Duh.
Maybe it is a complaint, but it's still a fact of life: your code is not reusable without wrapping it in C.
The reality is that C++ is not as reusable as C is: ever wonder why there are so few reused libraries written in C++, while they are so many in C?
Look on your system now - it's filled with C libraries, while the C++ libraries are probably a rounding error.
I also prefer apples over brooms.
Sometimes you do not want, or need, all the batteries.
I don't use any dynamically allocated memory in my firmware projects.
I do sometimes use statically allocated pools for things like packet buffers and allocate chunks out of them, but their lifetimes are not scope based, so automatic call of constructors/destructors would not be of any help.
I'm also wondering if you've heard about std::span, given your use case. I would be surprised if you weren't rebuilding much of that functionality.
Unless you're a solo developer, this is not a win.
It's no accident that C++ is the only language in which its proponents have to self police their own teams to only use a subset of the language.
How about capture of values in a lambda within a loop? How to prevent a template expansion from killing you build process? When are move semantics sufficient for the std container classes and when are they not? What's the order of construction of multiple objects at file scope? When should a copy constructor be written so that the default shallow copy is prevented?
All of those are footguns, because if you do them wrong the program has runtime bugs without any compiler warnings.
They are all absent from C.
I'm typing on a phone, so won't go into detailw, but if you want to make such an insane claim, bear in mind that Scott Meyers himself said that C++ is too complex for him
You are not disagreeing with me when you make that claim, you're disagreeing with one of the world's foremost experts on C++.
But you have an uncontrollable urge to write here? Someone's holding a gun to your head?
> You are not disagreeing with me when you make that claim, you're disagreeing with one of the world's foremost experts on C++.
I guess that settles everything then. Never mind that you're misquoting him.
Look, if you hadn't written the last two paragraphs, I'd have replied to your points, but they strongly indicate it would fall into deaf ears. You're clearly more interested in entertaining the peanut gallery more than actual discussion.
I'm not misquoting him - you are free to provide a link to the context in which he said what he said.
The worlds foremost expert in C++, author of dozens of books on C++, disagrees with you. I'm merely agreeing with him.
> Look, if you hadn't written the last two paragraphs, I'd have replied to your points, but they strongly indicate it would fall into deaf ears. You're clearly more interested in entertaining the peanut gallery more than actual discussion.
The fact that you entered a thread about C practices, then got all salty when you tried to go with the "but why not use C++?" argument, then devolved into personal attacks is ... well "classy" is not the word I'd use.
EDIT: You can't respond to those points - those are all well-known footguns that are present in C++ but not in C. What were you going to respond with? "No, C++ doesn't have those!"?
Simplicity beats complexity almost every time.
I think those definitions go to a header file. But how different will it be if he use existing types with an abbreviation system? And is this feature available with some IDE?
But one thing that makes it worth it is the removal of the struct tag space. I have a strong dislike for the struct tag boilerplate in C, but the alternative -- typedef boilerplate -- in C is unbearable to the point that I have a macro to define structs in C that does this automatically.
#define STRUCT(name) typedef struct name name; struct name
STRUCT(Foo) {
int x;
int y;
};
But macros often come with disadvantages. In this case it's that many IDEs have trouble finding the struct definitions from a usage site.He says so himself:
> I don’t intend to use these names in isolation, such as in code snippets (outside of this article). If I did, examples would require the typedefs to give readers the complete context. That’s not worth extra explanation. Even in the most recent articles I’ve used ptrdiff_t instead of size.
You require extra work to understand his basic types before reading even a short snippet, so he doesn't use it when he wants people to read short snippets.
Introducing additional stuff the programmer must remember that does not add any safety is pointless busywork.
A non-complete summary of his conventions:
1. typedef standard typenames to 3-char symbols,
2. remove qualifiers like const,
3. use macros to reduce the amount of typing the programmer does,
4. typedef all structs (and enums too, I assume)
5. A macro-ized string-typed with prefixed-length.
> Starting with the fundamentals, I’ve been using short names for primitive types. The resulting clarity was more than I had expected,
This isn't clear: `int8_t` is a lot clearer to a C programmer than `i8`, because a C programmer has already internalised the pattern of the stdint.h types. This is going to lead to subtle bugs as well: quick, according to his convention, what is the % specifier for `byte`?
You can use %c, but that gives you an ascii character (which is not what we think of when we say 'byte').
If you use PRIu8 the compiler might give warnings because `char` might be signed. The best option is to just not use `byte` and use `uint8_t` instead (or, in his system, `u8`).
Same with `b32` vs `i32` - it's a distinction without a difference and mixing these types won't give compiler warnings, while it is almost certainly an error on the part of the developer. Use `bool` if you don't like `_Bool`.
In general I try to take advantage of whatever typing C provides; I don't try to subvert it because I want the compiler to warn me when my intention doesn't match the code I wrote.
> No const. It serves no practical role in optimization, and I cannot recall an instance where it caught, or would have caught, a mistake.
I disagree with dropping `const`.
1. It's useful as an indicator to the caller that the returned value must/must not be freed. It's a convention I use that makes it easy to visually spot memory leaks.
Of the two functions below, it's clear to me which one needs the returned value `free()`ed and which one doesn't.
const char *replace_substring (const char *src, const char *pat, const char *replacement);
char *replace_substring (const char *src, const char *pat, const char *replacement);
2. It actually does catch a lot of problems, because the compiler warns me when I attempt to modify a value that some other code I wrote never intended to be modified. It's about intention, and when I know it is safe to modify the `const` value, then I have to explicitly cast away the const to compile my program. Anyone reading the program will know that the modification of the const-qualifed value is intentional, and therefore safe.> #define s8(s) (s8){(u8 *)s, lengthof(s)}
This is interesting. I will try this out in my next project. I do think that there'll be quite a few compiler warnings for sign-mismatch though. This is the second "I wonder what the sign is" question for programmers reading his code - it means that his code has to compile with the flags that he compiles it with (I assume he's passing a flag to force chars to a particular sign). You can't simply compile his code in another project unless you copy his flags, and those flags may conflict with the new projects flags.
I also wish that he'd showed a few examples of how having the length helps - what is presented in the post doesn't show any additional string safety over using nul-terminated strings. All those macros, including the one that creates the struct, could be written to operate on null-terminated strings. In essence, the length can be simply unused for everything! Where's the safety!?
> It’s also led to a style of defining a zero-initialized return value at the top of the function, i.e. ok is false, and then use it for all return statements. On error, it can bail out with an immediate return.
I use a similar pattern, but I use `goto cleanup` on all errors; you can't, as a general pattern, return early in a non-trivial C function without leaking resources. You can, as a general pattern, `goto cleanup` in every C function to clean up resources. I prefer the general pattern that I use everywhere rather than having to ensure that all resources acquired up to that particular return statement are released.
> rather than include windows.h, write the prototypes out by hand using custom types.
I think this is a very bad idea: you can't depend on the headers not changing after a compiler or library update. Sure, maybe in practice, all the Windows types and declarations don't change all that much, but I wouldn't want to be the developer trying to hunt down a bug because the interface to some function has changed and the compiler isn't giving me errors.
All in all, I dunno if I would look forward to working on a team with these conventions - the code is harder to read, doesn't work in isolation, needs custom flags, and introduces a string type without introducing any string safety with it.
Nice to see some evolutive convergence in C programmers, too
http://edit.rupy.se/?host=move.rupy.se&path=/file/game.cpp&s...
It's messy and in many ways ugly but it compiles and runs for an eternity.
and then you put the code in a header and everything explodes
The first section shows why some languages have no 'typedef': introducing another layer of aliases is just not a good idea. It's confusing, it changes the appearance to basically a new language. Just use the standard names, instead of redefining your language, like everyone else. This style is almost as bad as '#define begin {'.
Many of the other defs are obfuscations or language changes -- this coerces C to something else. I'd not like to read code written with this, as it heavily violates the principle of least astonishment (POLA).
(As a side note, I don't understand the #define for sizeof. The operator sizeof returns size_t -- it's size_t's definition, so what is this for?)
Their `size` type is signed. It's `ptrdiff_t`, not `size_t`.
That one is dubious. Char has magic aliasing properties that uint8_t might not have (iirc that was contentious in a GCC bug report) and it will be signed on some platforms and unsigned on others, which changes implicit integer conversions.
Missing from this is to embrace attribute((overloadable)) and attribute((cleanup)).
Overloadable is the sane, useful alternative to the thing standardised as _Generic. The C _Generic will let you define an overload set, with some weirdness around type conversions, provided you write the entire set out as a single _Generic expression, probably wrapped in a macro. If you want to dispatch on more than one argument, you nest _Generic expressions. If you want to declare different functions in different headers - maybe you want 'size(T)' defined on various types in the codebase - you can't. If you don't like the idea of thousands of lines of distracting nonsense in the preprocessed output, tough. Or - use overloadable, get open overload sets, minimal compile time cost, obvious intermediate IR, everything works. Prior art is all of C++, so talking decades of the tooling learning to deal with it.
Cleanup is either a replacement for raii, or a means to have debug builds yell at you when you miss a free. It looks like that got warped into a thing called 'defer' with different behaviour that didn't make it through the committee last time.
Other than that, ad hoc code generators work really well with C. Especially if you're willing to use some compiler extensions. Code generators + overloadable will give a fair approximation to templated data structures without going deep into the insanity of the preprocessor. If the overloadable functions are static inline forwarding things in a header they don't even mess up symbol names; you just get a straightforward translation to vector_float_size or whatever.
Personally I've given up on ISO C. I'd quite like to code in a dialect of C99 with a few of the GNU extensions and the equivalent of `fno-strict-aliasing`, but C with the pointer provenance modelling and an accretion of C++ features has no personal value. Currently still using clang with flags to make it behave like that but I'm conscious that's on borrowed time - the application performance friendly aliasing rules are the default and gaining popularity, and relying on opt-out flags is a means of opting into compiler bugs.
Semi-actively seeking something that will let me write assembly without the hassle of manual register allocation and calling conventions. Old style C with some of the warts bashed off would be good for that.
There's clearly life in the old dog yet!
I would have used isize instead, I think.
Everyone knows what a uint32_t is when they see it. The cognitive overhead (until it becomes second nature, obviously) just feels like a heavy price to pay in order to save yourself a few characters.
(Some other stuff in the proposed coding style still gets a thumbs up from me, though.)
I am not saying every style is good (some simply obfuscate things and/or make things overly verbose or unreadable) but rejecting a style solely based on it being "non-idiomatic" is not a good thing.
pg wrote excellent arguments against this criticism in "On Lisp" § 4.8 Density, which apply just as well to the discussion above:
“If your code uses a lot of new utilities, some readers may complain that it is hard to understand. People who are not yet very fluent in Lisp will only be used to reading raw Lisp. In fact, they may not be used to the idea of an extensible language at all. When they look at a program which depends heavily on utilities, it may seem to them that the author has, out of pure eccentricity, decided to write the program in some sort of private language.
[...]
If people complain that using utilities makes your code hard to read, they probably don’t realize what the code would look like if you hadn’t used them. Bottom-up programming makes what would otherwise be a large program look like a small, simple one. This can give the impression that the program doesn’t do much, and should therefore be easy to read. When inexperienced readers look closer and find that this isn’t so, they react with dismay.”Writing correct C is hard, so I’m not going to knock anyone who found stuff that helps them.
But pound defining shit to things you know via your Hungarian notion? Write some elisp. My Haskell programs don’t actually have Unicode lambda in them.
Pascal strings? Yeah, that’s probably the better call, but why not use C++ or Rust or something where a bunch of geniuses got it right already?
Arthur Whitney however is nuts.
I'll be diving into C fairly heavy for the first time ever next year. I intend to skip right past pascal strings and implement/use free pascal's AnsiString or UnicodeString, both of which are reference counted, have a length (with no limit) and are guaranteed null terminated. I've stored a gigabyte in them in a few milliseconds. There's no need to allocate or free memory either... it's like freaking magic.
I guess you're going to implement your from scratch, or is there some prior art you're likely to use?
I don’t see the harm in following this for your own passion projects. You aren’t doing it for the world, you’re doing it for yourself.
I also have often found myself publishing “for my own use only” code as open source later and like to keep things understandable to maybe help teach someone something someday.
You might not be old enough then :-P many codebases typedef their own int types. See glib (gint, gshort, gint32, etc), SDL (Sint32, Uint32, etc) off the top of my head and there are many that define types like "int32" or "i32" like the linked article.
Sure, the _t suffix may be an eyesore but I’ll take size_t over “size” any day.
b32, size (ptrdiff_t), usize (size_t), nothing for ssize_t... what? Those are unidiomatic and also kind of weird. The macros... some are fine, some are weird.
If this makes the author more productive in C, it might behoove them to see if a higher level language like Rust would meet their needs.
Undefined behavior[1]
> #define assert(c) while (!(c)) __builtin_unreachable()
Undefined behavior[1]
> I’ll cast away the const if needed.
Undefined behavior[2]
> The assignments are separated by sequence points, giving them an explicit order.
I don't believe assignments are sequence points and only the function call is.
[1] https://en.cppreference.com/w/c/language/identifier#Reserved...
And people keep telling me that nobody uses the C preprocessor to define their own syntax any more!
> Undefined behavior[2]
To be clear, it's only UB if the object was defined const, which is the case given he wrote:
> One small exception: I still like it as a hint to place static tables in read-only memory closer to the code. I’ll cast away the const if needed.
So you are correct on this point. Funnily enough, such objects are relatively rare IME, so I had to double-check to see that he was advocating it specifically in the rare case where it must not be applied.
Given that this particular undefined behavior usually causes crashes in practice, I expect the author is talking about casting away the const but not actually writing to the pointer. Which is legal.
My spidey senses tingle whenever I see const-ness cast away because it almost always means something is wrong. Either a function is missing a qualifier on an argument, or something very unsafe is happening. Why force callers to cast away const-ness in hopes that everything will be fine when you can just write the correct function signature.
And that is legal.
> Undefined behavior[2]
How so? As the page you linked mentions, simply casting 'const T *' to regular 'T *' is well-defined; it's only modifying a const object through the pointer that's UB (C17 6.7.3/7).
> I don't believe assignments are sequence points and only the function call is.
Assigments within expressions don't create sequence points. However, the expression of an expression statement is a full expression (i.e., not a subexpression of another expression), and there is a sequence point between each pair of full expressions (C17 6.8/4). In other words, the semicolons create sequence points.
(Haha, only serious.)
You missed the point of code style.