Lesser known tricks, quirks and features of C
jorenar.com
jorenar.com
It uses the constant expression to create a bitfield of size -1 when failed, and leaves the compiler to error on that as the intended assertion. The actual statement is an extern pointer to a function returning a pointer to an array which has sizeof the aforementioned bitfield struct as its size.
Another one encountered in Toybox is (0 || "foo") being a const expression that evaluates to 1. Apparently the string literal must have been soundly created in data section, so its pointer address is safely assumed to be non-zero.
Array pointers: Array to pointer decay is extremely annoying, if it was implemented as Array to "slice" decay it would be great.
Static array indices in function parameter declarations: awesome, a shame that C++ (and Tiny C) do not support it >/
flexible array member: extremely useful, and now there are good compiler flags for ensuring correct flexible array member usage
X-Macro: nice, no-overhead enum to string name. Didn't know the trick
Combining default, named and positional arguments: Named-arguments/default-arg, C version xD. It would be cool if it was added to C language as a native feature, instead of having to do the struct hiding macro.
Comma operator: really useful, specially in macros
Digraphs, trigraphs and alternative tokens: di/tri/graphs rarely useful, alternatives synonims of iso646.h are awesome, love using and/or instead of &&/||
Designated initializer: super awesome, could not use if you wanted C++ portability. Now C++ supports some part of it.
Compound literals: fantastic, but in C++ it will explode due to stack deallocation in the same line. C++ should fix this and allow the C idiom >/
Bit fields: nice for more control of structs layout
constant string concat: "MultiLine" String, C version xD
Ad hoc struct declaration in the return type of a function: didn't know this trick, "multi value" return, C version xD
Cosmopolitan-libc: incredible project. Already knew of it, its awesome to offer a binary that runs in all S.Os at the same time.
Evaluate sizeof at compile time by causing duplicate case error: ha, nice trick for debugging the size of anything.
The first array size is actually always decayed to a pointer, supporting it in a compiler without analysis passes like TCC is just a matter of skipping the "static" token and the size.
It's not just annoying, it's the major source of bugs in shipped code. A fix:
(In fact, I already had your article bookmarked xD, and I’m familiar with and truly admire your work)
It is what allows to do int * p = arr, and looping on array element with p++.
Keeping array type you would jump beyond the last element at first iteration.
int *p = &arr[0];
The mistake is to allow this: int *p = arr;C++ does?
void print(const int (&array)[5]) {
for(size_t i = 0; i < size; ++i)
std::cout << array[i] << " ";
}
will fail at compile time if you pass it anything other than an int[5] arrayvery interesting comment considering I'm literally fighting with stupid languages with this kind of permissive rules right now, which definitely just create more bugs (for instance silently dropped values because an upstream API changed, added an element at the end of the list, you updated but since you get no error you now have to go through all the calls to check them one-by-one)
If the size could a (type-level) variable, that would be a very different proposition. But variables lead to expressions, expressions lead to functions, functions lead to suffering^W becoming a full-fledged dependently typed programming language—if not an Agda or an Idris then at least an ATS[1]. I’d welcome that, but as far as I can see the ergonomics are very much not there yet for any kind of low-level programming.
I mean, that's just plain old C++. You could have compile-time expressions operating on compile-time numbers since the 90s ; it's a widely-used feature.
I've never tested to see if printf("%s", str) becomes the equivalent fputs(str, stdout)
#include <stdio.h>
int main(void) {
int count;
printf("%s%n\n", "hello, world", &count);
printf("count = %d\n", count);
}
The output is: hello, world
count = 12
%n can be exploited to write data to an arbitrary memory location, but only if the format string is something other than a string literal.%n can be exploited, but it's entirely possible to use it safely.
I've been digging into cross-platform (Windows and Linux) C for a while, and it has been fascinating. On top of that, I've been writing a JIT-ted scripting (templating) language, and the ABI differences (not just fastcall vs stdcall vs cdecl) are often not easy to find documentation about.
I've decided that if I ever get to teach a University class on C again, I wanted to cover some of these things that I feel are often left out, and this list is a helpful reference! Thanks!
Knowing these sort of techniques is important because they force you to think in different ways to solve a problem which expands one's mental design space. C (and C++) is particularly important here since it is the common "lingua-franca" across all system/application software from servers to desktops to itty-bitty MCUs.
PS: Also see the book Fluent C by Christopher Preschern which while not dealing with "tricks" shows how to use C effectively using a pattern-like approach.
> Backslash line splicing
One reason a trigraph was gone is that `??/`, a trigraph spelling for `\`, also acted like `\` in this context.
> Using `&&` and `||` as conditionals
Not only this is uncommon, but chaining them is not always correct because `a && b || c` doesn't equal to `a ? b : c` when `b` evaluates to false.
> Compile time assumption checking using `enum`s
Please use `static_assert` already.
> Matching character classes with `sscanf()`
This can be combined with `*` to ignore certain characters. For example `%*[ \t]` will skip all horizontal whitespaces, unlike a plain whitespace which will also skip newlines.
> Detecting constant expressions
This ultimately comes from C's weird way to say a null pointer, which is defined as any constant expression which type is inferred to be pointer. So a non-constant expression can be distinguished by multiplying it with a known zero constant.
OF COURSE it should do what \ does, otherwise you have no other way to get a \
the point of trigraphs is to allow characters to be entered that your character-set/terminal keyboard doesn't allow.
1. There is no real reason that trigraphs should be expanded inside a comment. Preprocessors can't make any additional comment, so the comment should be scanned and discarded as soon as possible but trigraphs somehow precede that.
2. And this very behavior of backslash should have been also deferred as much as possible. ISO C already has two sets of doubly-quoted literals `"asdf"`, where one is used for normal string literals and another is used for preprocessing because `#include "foo\bar.c"` should refer to the file name that contains a backslash, not a backspace (`\b`). Since `#include FILENAME` is also possible, such literals may appear anywhere in the preprocessing line! Therefore we already have to defer processing of some backslashes, so why should remaining backslashes be processed that early?
In my ideal design, a backslash is either a part of tokens (`+\<newline>=`, `foo\<newline>bar` or `"asdf\n\<newline>fdsa"`) or a standalone token optionally followed with a newline (`\<newline>`). No backslashes within comments are significant, effectively solving the first point. These tokens are then turned into normal tokens or whitespaces respectively, so they remain transparent to the parser. The trigraph could then have been allowed to replace backslashes in such cases (e.g. `+??/<newline>=`) without affecting remaining cases like comments.
For the record, later digraphs are more or less designed as such but they lack backslashes, even though ISO/IEC 646 still doesn't contain backslashes for all charsets. This hints that the inclusion of trigraphs or digraphs was more due to vendor complaints (known to be IBM) than actual concerns from users who wouldn't be able to type backslashes if it were true.
trigraphs are 100% substitutes for unrepresentable characters. they absolutely positively ALWAYS should be replaced by the character. Pretend it takes place before the character even arrives inside the comment, because it does.
it's very much like the #define/#include c-preprocessor step, it happens first, that's what keeps it clean, understandable, manageable. (Sure you can have more complex macro systems, but they are... complex, they can get very ugly)
if you know how to process a unix shell commandline, you know that there are layers to it. Trigraphs are just like that. If you don't know how a unix shell commandline is processed, learn it, it's worth knowing.
Also you should be aware that the macro expansion in C/C++ is not like a literal string replacement. `#define FOO bar` doesn't turn `BAREFOOT` into `BAREbarT` or `"OH FOO'S SAKE"` into `"OH bar's SAKE"`. (Some extremely old preprocessors did do so, by the way.) `#define FOO(x) FOO(x)` doesn't make `FOO(bar)` into an infinite recursion because `FOO` is prevented from expansion when `FOO` itself is being already expanded. There are certainly some layers, but they are not what you seem to think.
I was not talking about how the preprocessor is implemented, I was talking about the layering. You keep wanting to mix layers because you think you know better; thar be dragons.
And since we're thinking about layers, character encoding hacks should be entirely outside of a programming language responsibility. Now that would be a proper layering.
Such as:
typedef void fptr_t(int);
fptr_t foo;
That would effectively declare a function with the prototype: `void foo(int)'. This pattern is used quite a bit in BSD kernels.Really interesting to hear about the escaping quirk, I need to test it.
switch (n % 2) {
case 0:
do {
++i;
case 1:
++i;
} while (--n > 0);
}
Someone is really ought to record a "WAT" video about C.As for the footgun issue, I mean, it could be one in theory, sure. But I don’t think I’ve ever seen it actually fired. And I can’t really appreciate the Javaesque “abuse” thinking—it is to some extent the job of the language designer to prevent the programmer from accidentally doing something bad, but I don’t see how it is their job to prevent a programmer from deliberately doing strange things, as long as the result looks appropriately strange as well.
(There are reasons to dislike C’s switch statement, I just don’t think the potential for “abuse” is one.)
The relevant example code is;
int function(void) {
static int i, state = 0;
switch (state) {
case 0: goto LABEL0;
case 1: goto LABEL1;
}
LABEL0: /* start of function */
for (i = 0; i < 10; i++) {
state = 1; /* so we will come back to LABEL1 */
return i;
LABEL1:; /* resume control straight after the return */
}
}
becomes; int function(void) {
static int i, state = 0;
switch (state) {
case 0: /* start of function */
for (i = 0; i < 10; i++) {
state = 1; /* so we will come back to "case 1"*/
return i;
case 1:; /* resume control straight after the return */
}
}
}404
File not found
The site configured at this address does not contain the requested file.
> int (*ap1)[10] = &arr;
Wow that's garbage syntax. With Go it would be var ap1 *[10]int = &arrIt would be a time warp worthy of the Rocky Horror Picture Show if C's design could take syntax ideas from Go.
int *x[10]; // How is this different from `ap1` above?
int (*f (void))[10]; int *x[10]; ---> { int* } x[10];
int (*f (void))[10] ---> int { { (* {f (void) } ) } [10]; }
The point is that once you have had some practice you can work it out and Go's syntax is not necessarily much better.In any case, I think Go is a clear winner here because all logical types are consecutive tokens. For example `f` in my example is (as you correctly parsed) a pointer to a function that returns an array of 10 integers, but that return type is normally written `int [10]` or `int NAME[10]`, while here is written in two chunks `int` and `[10]` with a big parenthesis inside.
You need practice for complicated things and that is what i was pointing out with "can be worked out after some practice" and not that everything trivial needed practice.
"Go is a clear winner here" is your claim and not necessarily one that i agree with since as mentioned, knowing the binding rules and a little practice complicated declarations are not that big of a deal.
Absolutely baseless claim.
The C syntax and language is the product of a small group (not a committee) of smart people with the goals of syntactical brevity, close to machine architecture (PDP-7/11) and the design goal of building along the path of BCPL->B->C. Dennis Ritchie himself explains the rationale in his paper The Development of the C Language and so one does not need to make untenable assumptions. The enduring success of the language (even in the face of all the developments since then in Computer HW and PLT) is proof of the validity of its design goals. Its "Abstract Machine" is simple and there is no complicated Object Model with the syntax merely being a thin veneer over a sequence of bytes. Contrast it with most modern languages (which seem to be designed to solve world peace/hunger and everything in between) and C appears more and more relevant these days. C++ used judiciously without a lot of "new features" introduced by the standards committee (the bane of the language) takes it to the next level sweet spot.
> [...] In all these cases the declaration of a variable resembles its usage in an expression whose type is the one named at the head of the declaration.
> The scheme of type composition adopted by C owes considerable debt to Algol 68, although it did not, perhaps, emerge in a form that Algol's adherents would approve of. The central notion I captured from Algol was a type structure based on atomic types (including structures), composed into arrays, pointers (references), and functions (procedures). Algol 68's concept of unions and casts also had an influence that appeared later.
Algol 68 by the way had a sensible type syntax that is akin to Go's. Ritchie "explains" the syntax difference by having the same syntax for the type and the expression, but that's not exactly why. For example there are actually two expressions relevant here, `*` dereference and `&` reference. Why was the former used? Why couldn't `*` be made a postfix at this point if the same syntax was a big concern? Ritchie himself never fully elaborated on those points, and I think that's because it was never his primary concern at all.
As you have noted, it is very important to realize that C did have their design goals (for which C did a very excellent job). On the other hand it would be very misleading to claim that C had some bigger visions even at that time and they validate its design! Ritchie and others were clearly smart but they didn't design C to be something magnificant, it was a rough product from various trade-offs they had to endure. So why this particular syntax? Well, because B already picked a prefix `*` which Ritchie didn't want to change at all, and he allowed it to infect all other aspects of the type syntax without much consideration. (Or more neutrally, Ritchie couldn't figure out any other option when he had to keep the B compatibility. But keep in mind that B have already changed that operator from BCPL.)
Technically speaking it was somehow designed, but only by following the path of the least resistance, not a solid reasoning, hence my use of the word "accidental". There are many other examples, such as the logical `&` (etc.) being renamed to `&&` but the bitwise `&` keeping the original precedence order because nothing was done. To be fair to Ritchie though, it is not his fault, but rather a fault of the whole software community that was fixated on this ancient language designed for specific purposes way too long.
For each object of such a composed type, there was already a way to mention the underlying object: index the array, call the function, use the indirection operator on the pointer. Analogical reasoning led to a declaration syntax for names mirroring that of the expression syntax in which the names typically appear. Thus,
int i, *pi, **ppi;
declare an integer, a pointer to an integer, a pointer to a pointer to an integer. The syntax of these declarations reflects the observation that i, pi, and ppi all yield an int type when used in an expression. Similarly, int f(), *f(), (*f)();
declare a function returning an integer, a function returning a pointer to an integer, a pointer to a function returning an integer; int *api[10], (*pai)[10];
declare an array of pointers to integers, and a pointer to an array of integers. In all these cases the declaration of a variable resembles its usage in an expression whose type is the one named at the head of the declaration.The above can be summarized as the following two points;
1) "Declaration reflects Use" (from K&R C book)
which leads us to the corollary,
2) Syntax is variable-centric rather than type-centric i.e. You look at how the variable is supposed to be used and then work out its type.
> To be fair to Ritchie though, it is not his fault, but rather a fault of the whole software community that was fixated on this ancient language designed for specific purposes way too long.
Again, this is completely baseless and merely your opinion. I have already pointed out the main design goals which drove its design and the fact that it is still relevant today is proof of that. The "simplicity" of its very design is its greatest strength. The fact that modern needs (eg. Strong Type Safety, Multi-paradigm, Security, Concurrency etc.) require us to design/seek out more language features is not a reflection on the language itself since it was designed well before these "wants" became "needs". On the other hand the various extensions of C (eg. Objective-C, Concurrent-C, Handel-C etc.) are proof of its versatility and extensibility and hence its enduring relevance.
> On the other hand the various extensions of C (eg. Objective-C, Concurrent-C, Handel-C etc.) are proof of its versatility and extensibility and hence its enduring relevance.
Most popular enough languages will eventually have tons of extensions, most of which would be obscure and not known to the general public though. I'm not even sure which "concurrent" C you are talking about (there had been multiple extensions of the same name in the literature, none of them ever gained traction in the industry). Have you ever actually seen or used suggested extensions firsthand?
Also extending a language is actually kinda easy and doesn't imply some kind of quality. You seem to value your own decade-long experience, but I had been also a programming language researcher decades ago and I know that because any PL researcher will make new languages all the time. Or let's put in this way: Brainfuck has been extended and adapted so many times [1]. Does that make Brainfuck "versatile and extensible" in your definition? Is the enduring relevance even a necessary consequence of such qualities? Think about that.
> Thus Asking questions like "why was * used for pointer syntax" is meaningless since Ritchie himself says he took it from B; [...] Thinking about the evolutionary path BCPL->B->C answers your other questions.
It rather means that Ritchie took it from B and didn't feel like it should change. It should be noted that B did change its dereference syntax from BCPL, which eventually settled on `!e` and `e1!e2` for `*e` and `e1[e2]` in C (with everything implicitly being a byte pointer). As far as I'm aware there is no documented reason why Thompson turned `!` into `*`, which by the way would be used in too many places at this point until C moved away from BCPL-style escape sequences in string literals. Maybe Thompson's other language, Bon, has some clue but I have no information about that. In any case Ritchie clearly didn't think far into this particular change, because C was originally conceived as a largely compatible extension to B (then NB) and then stuck. Isn't that accidental enough?
> Now you can see how "Declaration reflects Use" and variable-centric syntax makes sense.
There are multiple concrete implementations for that driving principle. `int a;` and `a int;` would have been equally okay under this principle, so the only real reason to pick the former is the influence of B (`auto a;`). I wondered that you are using that term only to mean C's strictest implementation, and that you actually don't like ANSI/ISO C's newer function declaration syntax, but then your arguments for the general principle would not back the eventual syntax used by C.
> Nothing comes close to the "C family of languages" by which i mean not just extensions but anybody who took inspiration from it.
There are many different classes to consider. In the broadest sense even Python is said to be inspired by C, even though it would be absurd to consider Python to be a proof of C's inherent quality in addition to its popularity. Some languages are also only syntactically similar to C because its block syntax did have some nice syntactic property (known as "curly-brace languages" nowadays). Those superficial similarities can't imply your claim.
> There is only one "official" one, that was designed by Narain Gehani and explained in the book "The Concurrent C Programming Language".
It was never standardized, and apparently it wasn't available much outside of AT&T Labs. If the book would make it somehow official, I can write and publish my own book with the same title today. In any measure, that Concurrent C language is not as notable as other concurrent languages based on or influenced by C.
> By design and accident C turned out to be a pretty good Core/Kernel language for others.
There are a lot of extension languages that are NOT based on C or C++. In fact, I believe every single language in the TIOBE Top 20 ranking has at least 10 notable ones in average. C was used for extension only because it was popular for many years, and many such extensions had to work around or bend its limitations for their purposes. (For example, C itself doesn't have types for distinct address spaces and many extensions add reserved keywords, which you can add to any existing languages as I noted earlier.)
> It rather means that Ritchie took it from B and didn't feel like it should change ... Isn't that accidental enough?
That is not called "accidental" but a "design decision".
> In any case Ritchie clearly didn't think far into this particular change,
That is just your opinion and not borne out from Ritchie's own writings and what we can infer from it.
> `int a;` and `a int;` would have been equally okay under this principle,
No, the decision to have "int a" was already done and only a "pointer to int a" was being thought of leading to a variable-centric view.
> Some languages are also only syntactically similar to C because its block syntax did have some nice syntactic property (known as "curly-brace languages" nowadays). Those superficial similarities can't imply your claim
https://en.wikipedia.org/wiki/List_of_C-family_programming_l...
> In any measure, that Concurrent C language is not as notable as other concurrent languages based on or influenced by C.
The point was not popularity nor standardization but the domain into which C was extended. That is why i gave the three specific examples one for each important domain.
> There are a lot of extension languages that are NOT based on C or C++.
That is not the point. I am talking about using C as a "Core/Kernel language" in the design of other languages. No other language (other than the Lisp family) comes close to C in versatility and eventual success here.
C is not relevant any more, not sure what world you are living in. it only has any relevance because it was the best option at the time decades ago, and so people are forced to use it when making syscalls. thats it.
C is still in the top five in the TIOBE index today.
first of all, no. plenty of OS are made in other languages. also, the big OS WERE written in C, and only remain so in order to avoid redoing millions of lines of code.
> and then a interface to them are given through wrappers in other languages
again this is only done because the OS is using an outdated language, so people are forced to work with it.
> C is still in the top five in the TIOBE index today
that doesnt matter, this does:
"you're wrong man, C totally has a bunch of code being used thats private, I swear". you could say that about every single other language. only thing that matter is what can be measured. C is dead man, you are just in denial. its an old crap language that hasn't been relevant in at least a decade. if you need some evidence, just look to the fact that after decades it still doesn't have a package manager, so many people laughably just vendor code when working with C projects.
You could write even more complex declarators (but don't have to), but that would not prove that some other syntax is inherently intuitive. Case in point, I cannot parse the Go syntax as I do not know Go.
In my experience pointers to arrays are rather uncommon and I'm not sure that I've ever written a function returning one, having even less of a need for a pointer to such. (Thus out of all these, only your first example is somewhat common in practice.)
Or you probably never even tried. You should be immediately able to parse it if I provide a hint that `*`, `&` and `[10]` mean roughly the same thing as C, because `*[10]int` has no reasonable reason to be parsed as an array of 10 copies of something. You can't do so in C.
Someone is a fan of Doug DeMuro.
What value does the following program return?
int main()
{
int *p = 0;
loop:
if (p)
return *p;
int v = 1;
p = &v;
v = 2;
goto loop;
return 3;
}
Also, rather than doing `sizeof` via one error at a time, it's better to just emit them to a char array {'0' + sz/10, '0' + sz%10, '\0'}. Generalizing this to signed numbers of arbitrary size is left as an exercise for the reader.The only reason that might be surprising is that the "return *p;" statement refers to the value of an object at a point (textually) before its definition. But the lifetime of the object named "v" begins on entry to the innermost compound statement enclosing its definition -- in this case the body of "main".
Space for "v" is allocated on entry to "main". It's initialized to 1 when its definition is reached. The "return *p;" statement appears before the definition of "v" in the program source, but is executed after its definition was reached at run time, and within its lifetime.
It's important to remember that scope and lifetime are two different things. The scope of an identifier is the region of program text in which the identifier is visible; for "v" it extends from the definition to the closing "}". The lifetime of an object is the time span during execution in which it exists; for "v" it extends from the time when execution reaches the opening "{" to the time when execution reaches the closing "}". Formally, storage for "n" is allocated at the beginning of its lifetime and deallocated at the end of its lifetime. Compilers can and do optimize allocation and deallocation, as long as the visible behavior is consistent.
Aside: If "v" were a VLA (variable length array, introduced in C99, made optional in C11) its lifetime would begin when execution reaches its definition.
> An object whose identifier is declared with no linkage and without the storage-class specifier `static` has automatic storage duration [...].
> For such an object that does not have a variable length array type, its lifetime extends from entry into the block with which it is associated until execution of that block ends in any way. (Entering an enclosed block or calling a function suspends, but does not end, execution of the current block.) If the block is entered recursively, a new instance of the object is created each time. The initial representation of the object is indeterminate. If an initialization is specified for the object and it is not specified with `constexpr`, it is performed each time the declaration or compound literal is reached in the execution of the block [...]; otherwise, the representation of the object becomes indeterminate each time the declaration is reached.
That is, a local variable is live from the moment the block that contains its declaration is entered (however and wherever that happens) until it is left (ditto), but is initialized or, for lack of a better word, uninitialized each time execution passes that declaration (however many times that happens, including none). This is despite the fact that at compile time the variable’s name is not in scope until the = introducing its initializer (or the place where such a = would go if there isn’t one). Modulo its smaller feature set, C89 §6.1.2.4(3) stipulates the same.
In addition to GGP’s deliberately confusing example, this permits the much more reasonable and C89-compatible
switch (x) {
int i, j;
case 1:
/* use i and j */
break;
case 2:
/* use i and j */
break;
}
The only exception is locals of variably modified type (e.g. variable-length arrays), whose declarations you can’t jump over on pain of undefined behaviour.No wonder basically every C compiler allocates a single stack frame at function entry.
If you write:
{
int n = 42;
printf("%d\n", n);
}
in the abstract machine, `sizeof (int)` bytes are allocated on entry to the block and deallocated on exit, but a compiler can legally replace the entire block with `puts("42")` and not allocate any memory for `n`.Memory for objects defined in nested blocks is logically allocated on entry to the block, but compilers commonly merge the allocation into the function entry code. Even so, objects in parallel blocks can certainly share memory:
int main(void) {
{
int a;
}
{
int b; // might share memory with a
}
}
Logically, memory for `a` is allocated on entry to the first inner block, and memory for `b` on entry to the second inner block. Compilers will typically allocate all the memory on entry to `main`, but can use the same address for `a` and `b`.