Discussion about this post

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).

Zayd Krunz's avatar

C, my favorite functional programming language

4 more comments...

No posts

Ready for more?