6 Comments
User's avatar
lcamtuf's avatar

Since I've seen a lot of incorrect takes on these examples elsewhere on the internet, including on programming forums (!), here are the actual answers:

1) "Function definitions" - there are two things going on here. First, this is sort-of a K&R function declaration, *except* K&R function declarations are no longer accepted by GCC: "int x() int foo; { }" -> "error: old-style parameter declarations". You bypass this error if you don't give the parameter a name; I'm guessing this is a glitch in GCC, but we can have fun with it while it lasts. But that's not all: having multiple void parameters is nonsensical and is not allowed in modern function declarations: if you try "int x(void, void)", you'll get "error: 'void' must be the only parameter". So I'm guessing that's another glitch.

2) "Operator precedence" - this is a combination of four tricks. First, [[...]] is a special notation for attributes (a more concise and modern version of "__attribute__((...))"), but empty attributes are just skipped. Second, "typedef int x" can be rewritten as "int typedef x" - don't ask. Third, depending on the context, && can be a GNU extension to get the address of a label, or a Boolean AND operator; in the "puts" line, the first && is an unary operation on a label, and the other two are AND (you could also do "$:&&$&&&&$&&puts(...)"). Fourth, the same symbol ($) can be simultaneously used as a global type name, a local variable name, and a goto label name, without errors.

3) "Goto statements" - this combines three things. First, goto *foo is a GNU extension to jump to a stored / computed label address: "void* ptr = &&some_label; goto *ptr". Second, multiple independent expressions can be separated by commas. Third - and adding to the confusion - in this particular context, * has lower precedence than the comma, so "goto *a, b, c" parses as "goto *(a, b, c)", which is functionally similar to "a; b; goto *c". Meanwhile, "x = *a, b, c" would parse as "(x = *a), b, c".

4.1) "Counting and adding" - two tricks in the first example. The order of operations in calculating function parameters is unspecified, so printf("%d, %d, %d\n", i++, i++, i++) may produce "1, 2, 3" or "3, 2, 1"; in GCC, you get the latter. But that doesn't explain the entirety of the output, because we also have a fourth value: we should be getting "3, <?>, 2, 1", but we get "3, 2, 1, 0" instead. The added trick is that "union {} x" allows you to define an empty (0-length) type in a roundabout way, even though you can't do this in a seemingly more direct fashion ("void x"). But when you pass a zero-sized type as a function parameter, nothing is actually stored and no code is generated, so printf() is called with 3, <nothing>, 2, 1, causing an uninitialized register (usually zero) to be used for the fourth %d.

4.2) Second adding example: because union {} is zero-sized, this means that if we create an array, the address of the first element is the same as the address of the tenth element: &foo[1] == &foo[10]. Because of the quasi-equivalence between arrays and pointers, this also means that adding a number to a pointer to a zero-sized object will simply disregard the numerical operand: foo + 1 == foo + 10 (and 1 + foo == 10 + foo). Note that this isn't what happens with void*; adding 1 to a void* moves the pointer by one byte. This means that in the latter case, the apparent size of the referenced object - sizeof(void) - could be actually understood to be 1 (and while it's non-canon, you'll get that result in GCC and clang).

mids's avatar

Except for the first one, which is a remnant of old-style function definitions, all of these are some non-standard GNU extension, and will scream at you if you add -pedantic on Clang. The counting one is extra spicy as it will simply behave differently across different architectures and versions of the same compiler.

The final one is also funny, because `(union {}*)2 + 2` should behave the same as `(void*)2 + 2`, i.e. the result should be 2, because the sizeof of the type is 0, but as a GNU extension, arithmetic on void pointers is treated as sizeof(void*)==1, and this is such a common extension that in most compilers the result will be 4...

Zayd Krunz's avatar

C, my favorite functional programming language

Miguel Lechón's avatar

Thanks for your effort cataloging these advanced techniques. You're an inspiration!

Adrian Bergeron's avatar

The more I learn about computers and coding, the more it seems like something between luck and magic, but your writing does help clarify.