Mildly interesting quirks of C
gist.github.com
gist.github.com
http://www.pvv.org/~oma/DeepC_slides_oct2011.pdf
Source: https://freecomputerbooks.com/Deep-C-and-Cpp.html#downloadLi...
Previous discussion: https://news.ycombinator.com/item?id=3093323
It could be considered a bit dated at this point (It's before C++11) but I find it still both entertaining and educating.
These days they have diverged even further. So I find funny to see C/C++ on job listings like it's one thing. While you can write code in a very C like manner in C++. That is not how typical C++ is often written these days.
a[5]
is the same as: 5[a]
why? a[5] is actually sugar for *(a + 5), so by commutative property, you can also do *(5 + a) to access the same memory position :-)Array-to-pointer decay is another manifestation of this.
https://cil-project.github.io/cil/doc/html/cil/cil016.html
Also: https://cil-project.github.io/cil/doc/html/cil/cil012.html
People who don't know what "simple" means and confuse it with "easy".
https://www.entropywins.wtf/blog/2017/01/02/simple-is-not-ea...
https://www.infoq.com/presentations/Simple-Made-Easy/
"Easy" things almost always lead to astonishing complexity.
Also it's easy to see just how complex C is: Have a look at a formal description of it! (And compare to a truly simple language like e.g. LISP).
https://github.com/kframework/c-semantics/tree/master/semant...
In contrast some basic Lambda calculus language semantics fit 0.5 of a page in K.
"simplicity is the ultimate sophistication." -- Leonardo da Vinci
What in the world…!!
It’s in the GCC section so I assume it’s some kind of lambda-function-like compiler extension? That allows jumping between bodies of two different functions…!
struct foo {
char a;
long b: 16;
char c;
};
So, a has been laid into the structure, so the current offset is 1 byte.
This is considered to be occupying a portion of an existing long type bitfield cell. In other words a is essentially taken to be an 8-bit field in the first long-sized cell of the structure. That cell looks like it has 56 bits left in it (if we assume 64 bit long). Since 56 > 16, the new bitfield b is placed into that cell. When that field is placed, the placement offset becomes 3. The type of c being char, that offset is acceptable for c.I've painstakingly reverse engineered the rules when developing the FFI for TXR Lisp:
1> (sizeof (struct foo (a char) (b (bit 16 long)) (c char)))
8
2> (alignof (struct foo (a char) (b (bit 16 long)) (c char)))
8
3> (offsetof (struct foo (a char) (b (bit 16 long)) (c char)) a)
0
4> (offsetof (struct foo (a char) (b (bit 16 long)) (c char)) b)
** ffi-offsetof: b is a bitfield in #<ffi-type (struct foo (a char) (b (bit 16 long)) (c char))>
4> (offsetof (struct foo (a char) (b (bit 16 long)) (c char)) c)
3
I've summarized my empirically-obtained understanding for the benefit of users and anyone else doing similar work in a different project.If a leading char member is followed by a uint64_t bitfield that is 57 bits wide, a new cell will be allocated for those 57 bits at offset 8. The char is considered to be a field of 8 bits allocated in an existing 64 bit cell, leaving 56. 57 cannot fit, and so the offset is bumped to the next cell alignment.
This is testable.
I'm only writing about GCC, not about ISO C, which specifies very little, allowing implementations latitude in choosing the underlying storage unit size and alignment for bitfields regardless of their declared type.
struct X { char x[8]; };
struct X awoo(void);
printf("%s\n", awoo().x);
The above is UB in <= C99 and valid in >= C11. [0] struct X { char b[8]; } foo();
int *b = foo().b;
printf("%s\n", b);
The above is UB in >= C11 and valid in <= C99. [1][0] https://wiki.sei.cmu.edu/confluence/plugins/servlet/mobile?c...
[1] http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1285.htm
Depending on what you mean by undefined behavior, now you've made register allocation an invalid optimization. You really don't want to use that version of C.
UBs were added for cross-incompatibilities, where operations were too "core" (and / or untestable) for IBs to be acceptable. The reason was not performance (aside from not imposing a runtime check where that would have been possible) but portability:
> 3.4.3 undefined behavior behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements
Those UBs were leveraged later on by optimising compilers, because they provide constraints compensating for C's useless type system.
So you can just use a non-optimising compiler or one which only does simple optimisations (e.g. tcc), and see what the compiler generates from your UBs.
* There are more things in heaven and earth (MMIO!) than are dreamt of in your memory model.
struct bla_t { int a, b, c, d; } make_bla(void) {
return (struct bla_t){ .a=1, .b=2, .c=3, .d=4 };
}
https://www.godbolt.org/z/Pha7dPzeqAlso to be pedantic: "= {};" is not valid C (at least until C23) and fails to compile on MSVC - GCC and Clang accept it as a non-standard language extension though (the proper form would be "= {0};").
These days many compilers will warn if you do this, however, as it is rare people do this and usually indicates a misunderstanding of the type used.
I think it's quite readable though, so it's a shame it causes warnings. What do you think?
struct { const char *name; int age; } records[] = {
"John", 20,
"Bertha", 40,
"Andrew", 30,
}; 8. Modifiers to array sizes in parameter definitions [https://godbolt.org/z/FnwYUs]
void foo(int arr[static const restrict volatile 10]) {
// static: the array contains at least 10 elements
// const, volatile and restrict all apply to the array type.
}
I imagine most of these depend on the C version, but this one specifically bit me because one tool only supported c99 and the other was c11 or something later.What? UB is clearly undesirable, but assuming it is impossible and deducing other outcomes must be meant are clearly wrong assumptions by the compiler writer.
More sensible compilers (including older version of clang) do the right thing (TM) here and yield a compiler error.
There were earlier attempts at do-what-i-mean programming languages. They are rightfully buried in history.
It's debatable whether it's a good assumption. But not wrong.
Compilers can and absolutely do assume that UB is impossible in this code (no integer overflow) and deduce other outcomes must be meant (the loop operates on contiguous memory):
void foo(char* arr, int32_t end)
{
for (int32_t i = 0; i != end; ++i)
arr[i] = 0;
}
(Based on code from the gist comments.)It is exemplified by the C++23 std::unreachable() function, its description is "invokes undefined behavior". It is intended to mark part of the code that are unreachable, so that they don't appear in optimized builds, but may appear in debug builds. It is an explicit use of "the power of UB", an optimizing compiler considers that calling std::unreachable() is impossible, so all code paths that lead to it can be safely pruned. In an debug build, the code may be generated anyways, and the compiler will chose something sensible for what happens when it is called, typically a crash, but it can be anything, it is UB.
void foo(int p[static 1]);
is effectively a standard way to declare that p must be non-null pointer. I always wondered if any compiler actually makes use of this for optimization purposes.But OK, I understand that my mind is just not made for the complexity of C. Most likely I'm not a real programmer.
I get instantly knots in my brain and start to bang my head against the wall when I need to look for too long on C code. Actually even C documentation is enough to trigger this. (I get mad every time I have to look on a Linux system man page).
This is highly subjective of course. Other people seem to love C!
I'm more of a grug brain¹, who mostly only understands plain pure functions.
Input in, output out. No magic. Everything else's too taxing.
#2 and #5 can be combined to make and interesting hack. When combinded with memcpy you can do
int *a = memcpy(&(int){0}, b, sizeof *b);
C23 typeof makes this even more interestingIf you what an challenge here is a standard compliant c code. Try to undestand it. If can understand you are a master of c's type system
static int* (*const *(*restrict x)[5])(volatile union {struct{int a;int b;};}[static const restrict 5], register enum{HELLO,WORLD} a) = {0};I think this reasoning is slightly incorrect, although I don't blame the author as this is a very common misconception. I believe the correct reasoning might be as follows:
1. The compiler sees the pointer is dereferenced.
2. The compiler infers the pointer was not NULL.
3. The compiler determines a set of candidates for its target (which may be the universal set).
4. If it finds only one candidate, it just substitutes the target.
The critical thing to notice here is that the compiler doesn't need to care about the reachability of that candidate. It's making a conservative over-approximation, after all. You can witness the effect of this by formulating an impossible condition inside bar() that the compiler completely ignores: see [1]. Note the pointer assignment cannot have been implied by "bar() will have executed", as the execution of bar() could never lead to that assignment anyway!
WTF. Here have some keyword soup, the order doesn't matter at all. Must be fun to write a C compiler.
TIL that a dynamic array is also called flexible. This generation, out of boringness, is trying to redefine well established paradigms? Because, for me, a 90's formed developer, "flexible" means maybe inheritance, or even better polymorphism. There is nothing flexible about a dynamic array. Its structure is well defined in the stack/heap, and with current compiler optimizations can even be demoted to a simple static array for faster access within CPU registries.
"Flexible array member" [0] is when you have a struct and its last member is an array with unspecified size.
An example:
#include <stdio.h>
#include <stdlib.h>
struct Foo {
int len;
int* arr; // dynamic "array"
};
struct Bar {
int len;
int arr[]; // FAM
};
int main()
{
const int n = 12;
// have to allocate myself; no guarante it will be nearby the rest of struct
struct Foo* a = malloc(sizeof a);
a->arr = malloc(n * sizeof *(a->arr));
// array is part of the memory allocated for struct
struct Bar* x = malloc((sizeof x) + n*(sizeof *(x->arr)));
return 0;
}
[0]: https://en.wikipedia.org/wiki/Flexible_array_memberNo. A dynamic array is an array which can be expanded or shrinked during its runtime life. The fact that C/C++ uses malloc for that (and btw, it's not the only way to do it) it's her problem. In other languages you have dynamic arrays that can be expanded/shrinked without using an extra line - main reason why nowadays Rust is a replacement for C/C++
>[0]< From you own wiki reference: "the flexible array member must be last"
LMAO, really? Well, that indeed is a bigger C quirk. In Pascal, as an example, I can have it anywhere inside the record (struct equivalent of C), and it can be just as "flexible".
I've been doing C a long time and thought I knew all the "decent" tricks. When I saw it, I went "Oh, that's one of the silly new dynamic features that I ignore."
Nope.
It's been in the language forever. I'm surprised I never tripped over it before given all the embedded work I do.
The "compound literals are lvalues" pattern I've seen many times for inline initializing a struct that's only going to be around as a parameter to a single function call.
If `x` is not a constant, `(void*)((x) * 0l)` is a void pointer to address 0 (which may not even be a null pointer at runtime, since null may have a runtime address distinct from zero!). The ternary conditional then unifies the types of the branches, resulting in `void*`.
C and Rust don't perfectly overlap, especially since Rust is more a replacement to C++ than C.
So whatever language Rust "replaces" is a kind of moot point, and then there is the whole ongoing integration with Linux, a UNIX clone kernel.
Fortunately GCC has a whole bucket-list of warnings that can be enabled (I like compiling with -Wall -Wextra -pedantic, myself) which can, combined with proper tooling, catch many issues.