Hacker News new | past | comments | ask | show | jobs | submit login
Lesser known tricks, quirks and features of C (jorengarenar.github.io)
309 points by frankjr on July 1, 2023 | hide | past | favorite | 94 comments



The register keyword is still useful in compiler specific contexts. e.g., for GCC-ish compilers like gcc, clang:

    long result;
    register long nr __asm__("rax") = __NR_close;
    register long rfd __asm__("rdi") = fd;
    __asm__ volatile ("syscall\n" : "=a"(result) : "r"(rfd), "0"(nr) : "rcx", "r11", "memory", "cc");
The above is basically how you might implement the close(int) syscall on x86-64.

You don't need to be doing embedded programming to find it useful to dip down into assembly like that (though syscalls are perhaps a bad example, even for a syscall not provided by your C library -- that library probably provides a `syscall` function/macro that does all this in a platform agnostic way).

Also, "%.*" is extremely useful with strings, i.e., "%.*s". Your code base should be using length delimited strings (basically `struct S { int len; char* str; };`) throughout, in which case you can do `printf("%.*s\n", s.len, s.str);`


Um, no. You could could remove the register/__asm__("reg") qualifiers entirely and just specify "D" (rfd) as an input parameter and the code would work fine. There is absolutely no need for register there.

The only good use of register these days is for project-wide globals in embedded contexts. IIRC one example of this is the decompilation of Mario 64, where a certain register ALWAYS contains the floating point value 1.0.


Both are valid, but I much prefer the `register` method I gave (documented in https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables....), as it is far more self-explanatory. GCC's extended asm syntax has too many inscrutable constraint and modifier codes even excluding these GCC-ext-asm-specific codes to reference machine-specific registers by name. As such, I totally disagree with your statement about "the only good use". Given that I first learned about that method of specifying registers by reading linux kernel source code, I think others would disagree as well.


One of the reasons for gcc's inline asm syntax being so verbose is it tells the compiler which registers are used and how. There is no indication in your last asm() that the value of rax before the asm() is read from. This means the compiler could assume it's safe to clobber rax just before. After all, you assigned a value into rax and never read it, a reasonable optimizer might say, why emit that first assignment at all?


I think you should read https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Input-O... and the link I gave earlier, if you think my example is something novel of my own construction. It's basically straight from the GCC docs.

If your point is that I don't use result, that's because it's a snippet written into Hacker News. I didn't write the code to convert it into an errno and return -1 on error, etc., but doing so would be perfectly valid, and safe from your reasonable optimizer concerns.


I see absolutely no examples in that link there where they assign into a register via C code, not using it anywhere else, and then assume you can read the same value back from that register in an asm() statement without declaring it as an input.


I referenced both links (the latter links to the former, and the former, which was in the first comment of mine you replied to, contains the following examples:

    register int *p1 asm ("r0") = …;
    register int *p2 asm ("r1") = …;
    register int *result asm ("r0");
    asm ("sysint" : "=r" (result) : "0" (p1), "r" (p2));
and

    int t1 = …;
    register int *p1 asm ("r0") = …;
    register int *p2 asm ("r1") = t1;
    register int *result asm ("r0");
    asm ("sysint" : "=r" (result) : "0" (p1), "r" (p2));
In my example, nr is rax and listed in the input section, rfd is rdi and also listed in the input section, and result is rax and listed in the output section (I even used your preferred syntax for specifying rax here). Using result after the syscall asm statement is perfectly valid.


I think the GCC ASM syntax for specifying inputs and outputs is quite clear enough, and doesn't require a variable declaration with unusual syntax.


I'm merely referring to the fact that I specified rdi by writing "rdi" not "D". I can specify r10 by writing "r10", but I can't remember how to specify that directly in the inputs/outputs constraints -- a glance at https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html didn't show me how either, but I'm guessing it's there.

[Edit: Although, on second glance, from https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Input-O...:

> If you must use a specific register, but your Machine Constraints do not provide sufficient control to select the specific register you want, local register variables may provide a solution (see Specifying Registers for Local Variables).

indicates in the r10 case maybe you _must_ use the syntax I gave?]

My preference is for the syntax that requires looking up fewer tables in GCC docs, but as I said, the version you prefer is fine too.


Indeed, the register variable syntax is necessary for many of the registers; there are only so many of them that have been stuffed into the one-letter constraint. I've used it before for making raw x86-64 Linux syscalls (which can use r10, r8, and r9) without going through errno, as part of a silly little project ([0]) to read from a file descriptor without writing to process memory.

[0] https://pastebin.com/mepsedCC


Nice. Yes, linux/tools/include/nolibc has syscall macros that look near identical.


Even on x86, There are some registers where there isn't a corresponding exact input operand modifier, so register is the only option. But I forgot which register.


Just wondering... Why do they need to keep 1.0 in a register?


It's used in a lot of contexts, and it's faster to have it on hand rather than loading the constant from memory (which is slow on the N64).


D's inline assembler:

    asm {
      mov RAX,__NR_close;
      mov RDI, fd;
      call syscall;
    }
The compiler automatically keeps track of which registers were modified.


That's beautiful... Why can't GCC and clang have this?

Small note: your code seems to be calling a wrapper function rather than running the syscall instruction directly so it could probably be even more succinct. Is this valid D?

  asm {
    mov RAX, __NR_close;
    mov RDI, fd;
    syscall;
  }


SYSCALL is an actual instruction, and it's supported by the D inline assembler.

https://www.felixcloutier.com/x86/syscall

> Why can't GCC and clang have this?

I don't know. My brain bleeds out my ears every time I have to deal with that.


It really is painful.


> Why can't GCC and clang have this?

Because UNIX culture sucks at inline Assembly.

D's approach was widely used across PC, Amiga and Atari ST compilers.

Besides D, that is what Delphi, C++ Builder and Visual C++ still use nowadays, for the targets that still support inline Assembly. For 64 bit targets, they use intrinsics, which are still preferable to UNIX's approach.


> That's beautiful... Why can't GCC and clang have this?

There are tricky contingent reasons, like the fact that GCC proper doesn’t try and actually isn’t even able to parse assembly—and it would need to here in order to know that it can’t put fd in RAX or in any location whose address computation includes RAX.

(GCC’s codegen in general works by stitching together fragments of assembly as text and feeding them into the system assembler. Which is nice and UNIXy and all, but I’m not sure much is won by making the compiler agnostic to the actual bit patterns of the instructions when these days it is still aware of almost every other facet of the instruction set and quite a few microarchitectural details besides.)

But the fact of the matter is that GCC’s approach also permits better optimization, by telling the register allocator exactly which registers are clobbered by the inline assembly snippet and where the computations of the input should aim to place their results. You frequently end up with no MOVs at all, although Clang is better at this part than GCC.

(Do not be fooled into thinking that an automatic "register asm" variable is pinned to the specified register for all of its scope—it’s only guaranteed to be placed there at the start of any inline assembly blocks, the register allocator can move it out of the way in between them.)

I think the x86 code doesn’t actually need the "register asm" syntax at all, FWIW—you should be able to just add "a"(__NR_close), "d"(fd) to the inputs. GCC on RISC archs doesn’t usually define a separate constraint for each individual register, though, so on those it can be necessary.


clang does not support specific-register variables for registers which are allocatable by its register allocator (https://clang.llvm.org/docs/UsersManual.html#gcc-extensions-...) so this is gcc-only. If you care about portability between gcc and clang you'll need some other approach...


It doesn't support them for a specific kind of global, but it supports function local use like the one I gave above. Here's another example I tried just now:

    #include <sys/syscall.h> /* for __NR_exit */
    int main(void)
    {
        long ret;
        register long nr __asm__("rax") = __NR_exit;
        register long ec __asm__("rdi") = 99;
        __asm__ volatile("syscall\n" : "=a"(ret) : "r"(ec), "0"(nr) : "r11", "rcx", "cc", "memory");
        return 0;
    }
Build: clang -s -O0 -o ./t ./t.c

Disassemble to double check: objdump -M intel -D ./t

Run: ./t || echo $?

Output: 99

The docs you link basically say that the special global "1.0" register mentioned in https://news.ycombinator.com/item?id=36551728 wouldn't be supported in clang -- so much for the only good usage!


Whoops, thanks for the correction. My main experience with register variables is with the global kind -- QEMU used to use one to hold a CPU environment pointer, and we had to refactor to get rid of it in order to support clang. (By that point in time the register global was more of a legacy bit of design rather than something that was actively useful for performance or convenience, so it was a useful piece of cleanup to do anyway.)


Isn't `register` more a hint than guarantee? Likewise, is binding a variable to a named register (let alone any register) guaranteed to be respected?


Fun fact: the order of type qualifiers (const, volatile, restrict), type specifiers (char, int, long, short, float, double, signed, unsigned) and storage-class specifiers (auto, register, static, extern, typedef) is not enforced at the current indirection level. This means that the following declarations are identical:

    long long int x;
    int long long x;
    long int long x;

    typedef int myint;
    int typedef myint;

    const char *s;
    char const *s;

    const char * const volatile restrict *ss;
    const char * volatile const restrict *ss;


But preference in ordering immediately qualifies you as coming from the east or the west const.


I like the idea of someone sitting down and looking at someone else’s code, leaning back with satisfaction after they notice the programmer’s preference. “I like the cut of their jib.”


I do like the cut their jib as they place their asterisks next to the variable name rather the type, just as our forefathers intended!


You could write a Sherlock Holmesesque code review short story based on it.


I remember some really nice macro usage.


The %.* example is so close to hitting its single most useful application:

  char *something;  /* no null termination */
  size_t something_length;

  printf("%.*s", (int)something_length, something);
Unfortunately, the .* argument has type (int), not size_t, and it's signed… but if that's not a problem this is a great way to format non-\0-terminated strings.

(And of course you can also use it to print a substring without copying it around first.)


Somewhat related to this, printf alone in a loop is Turing-complete, by using %-directives like that. It was introduced in “Control-Flow Bending: On the Effectiveness of Control-Flow Integrity” (Carlini, et al. 2015) and the authors have implemented Brainfuck and an obfuscated tic-tac-toe with it.

[0]: https://nebelwelt.net/publications/files/15SEC.pdf

[1]: https://github.com/HexHive/printbf

[2]: https://github.com/carlini/printf-tac-toe


> While its primary purpose is to serve as The One True Debugger, printf also happens to be Turing complete.

Excellent.


In this simple case, if the int cast is a problem, fwrite would be an adequate alternative, don’t you think?


The point of this is that you can use it in format strings where you may be printing a whole bunch of other things.

In particular there are some APIs where you need to submit the whole format/string in one go, an example from libc is syslog(), but other libraries do this as well occasionally.


not for s(n)printf ...


For that, the analogue would be memcpy; but both alternatives lose the ease of surrounding the string with other text, since you either have to do the length calculations or define helper functions.


I "abuse" unions and anonymous unions all the time, it's very practical, and make the 'user' code a lot clearer as you can access the small 'namespace' as convenient. Here for example I can access it as named coordinates, x,y points or a vector.

   typedef union c2_rect_t {
     struct {
        c2_pt_t tl, br;
     };
     c2_coord_t v[4];
     struct {
       c2_coord_t l,t,r,b;
     };
   } c2_rect_t;


100% agree! I use whatever tool in the C toolbox results in the easiest-to-(read|grok) code, which means tons of anonymous union/struct “abuse”, bit fields, function pointer typedefs, strictly adhering to “Cutler Normal Form”.


What is "Cutler Normal Form"?


confused by the code,can you elaborate more


They can access the first coordinate either with c.l or c.v[0]. They are aliases to the same memory position, because of the union.


Also via c.tl.x (structure not shown but obvious). There are three fields in a union.


I used to use the comma operator to return a status code in the same line as an operation, but for some reason nobody liked my style.

   if (error_condition)
       return *result=0, ERR_CODE;
So, back to writing lots of statements.


I love this style (avoiding multiple-statement blocks) !

Sometimes you can still avoid multiple statements by rearranging your code otherwise. For example, in your case, you can set *result=0 at the beginning of the function. Other times, you can also cram the assignment inside the condition using short circuit evaluation; this trick somehow seems more palatable to normies than the comma operator.


Yeah, I felt that in cases (like when doing COM programming) where the true result is almost always returned in a parameter and the status code as the return value, it made a lot of sense to me to combine those into one line wherever they appeared. But, like the article says, this is a lesser-known operator. In libc style, a similar thing makes sense, e.g.

   return errno=ENOENT, (FILE*)0;
I don't know if anyone uses this style.

In K&R, they say that it's mostly used in for loops, such as

   for (i = 0, j = strlen(s) - 1; i < j; i++, j--) ...
So that is where I use it now.


I have used it c++ to take a scoped lock without naming it and returning a mutex protected value:

   return scoped_lock{foo_mux_}, return foo_;
Also nobody likes it.


Deosn't such an unnamed variable get's immediately destructed, right at the comma? I am pretty certain I'd hit exactly that problem and had to switch to __LINE__-macro to name such scoped locks.


My understanding of the standard is that the `full-expression` ends at the semicolon and 6.7.7.8 prescribes that the temporary shall be destructed after copying the value to be returned.


I think you mean:

    return scoped_lock{foo_mux_}, foo_;


Indeed! :)


This article was already discussed here: https://news.ycombinator.com/item?id=34855331 (410 points | 4 months ago | 176 comments)

But that link no longer works.


> Multi-character constants

I asked on SO why C characters use `'` on both ends instead of just one (e.g. why not just `'a` instead of `'a'`?). This seems to have been the biggest reason.


The main reason for that is practicality. '\n' and '\0' are also characters. You could somehow still parse it, but it would be less clear and possibly need more escaping.

Multi-character constants are historical baggage.


I don’t see the issue. If it’s a literal character it’s one character; if it’s `\n` or or `\0` then it’s two; if it is an octal escape it’s four; and so on.

You have to parse them the same way in a character literal as in a string literal, anyway.


> if it is an octal escape it’s four;

I just figured out that

1. `\0` and octal numbers share the same prefix

2. Octal numbers can have 1-3 digits (not fixed)

So maybe it’s more tricky than I thought.


An octal-escape-sequence is a backslash followed by 1, 2, or 3 octal digits.

'\0' is just another octal escape sequence, not a special-case syntax for the null character.

"\0", "\00", and "\000" all represent the same value; "\0000" is a string literal containing a null character followed by the digit '0'.

Hexadecimal escape sequences can be arbitrarily long. If you need a string containing character 0x12 followed by the digit '3', you can write "\x12" "3".


Thanks, that makes sense. :)


> Multi-character constants are historical baggage.

It is more that it is an implementation detail of some compilers that was then (ab)used by certain platforms.


Not relevant to C, of course, but Ruby supports something like this with `?a` being equivalent to `"a"` (both of which are strings, since Ruby doesn’t distinguish strings from characters). From what I’ve seen, it is recommended against in most styles, I assume because it is harder to read for most people.


In older days before Ruby had encoding-aware strings, ?a would return the ASCII integer value of 'a'. It made sense in that context but is now pretty much a quirky fossil.


  C11 added _Generic to language, but turns out metaprogramming by inhumanely abusing the preporcessor is possible even in pure C99: meet Metalang99 library.
I'm actually working on a library doing just that! It's still in very (very) early development, but maybe someone may find it to be interesting. [1]

Link [2] is the implementation of a vector. Link [3] is a test file implementing a vector of strings.

[1]: https://github.com/jenspots/libwheel

[2]: https://github.com/jenspots/libwheel/blob/main/include/wheel...

[3]: https://github.com/jenspots/libwheel/blob/main/tests/impl/st...


The downvotes make me laugh, I did something pretty similar not long after the _Generic keyword came out and remember getting a pretty icy reception even though I was pretty up front about how painful and crufty it is.

https://abissell.com/2014/01/16/c11s-_generic-keyword-macro-...


C++ Metaprogramming is also just a bunch sugarcoated preprocessor macros and it was never someting else.


The fundamental difference is the C preprocessor is actually a language independent of C. They follow their own grammars, syntax, semantics, symbol tables, etc., and do not communicate with each other.


This is plainly not true


This is the sprinkles on the icing of the five teir cake why C scares me. Thanks for sharing this, I'm sure it will help someone but I sincerely hope the never write C again.


> This is the sprinkles on the icing of the five teir cake why C scares me. Thanks for sharing this, I'm sure it will help someone but I sincerely hope the never write C again.

I looked through this list, and I gotta ask, which items exactly do you find scary? Most other popular languages have similar, if not worse, quirks than the ones in this particular list.


I'm not here to bash C, okay maybe a smidge, but yes other languages do have gotchas and weird ugly creepies. Some of the ones that scare me...

The '-->' nonoperator.

Defining structs in a function declaration gives me the creeps. Same goes for the nested struct definition interpretation like come on. Void pointers in general stress me out. That's one I knew about before reading this. Makes bookkeeping hard.

I get that C is basically sugar around ASM. I have respect for it. At the same time, it feels so hazardous to write. Everytime I think I know enough I stumble on some weird UB landmine, or find some bizarre library that despite understanding 99% of the code the remaining 1% conceals everything I care about.

The quirks in many other languages can be rough, but I feel like Cs leave developers in a scarier place given the risk factors


> The '-->' nonoperator.

it's the same in most languages. Works identically in Java, for example: https://www.jdoodle.com/iembed/v0/JL5

See?

> Defining structs in a function declaration gives me the creeps.

Both Kotlin and Go allow defining classes anywhere - they're anonymous.

> Same goes for the nested struct definition interpretation like come on.

Come on ... what? That's not a footgun. Because C is strongly typed, anytime you do a nested definition and expect it to be in the scope of the parent struct, you'll get an error if you try to create a new struct with the same name.

> Void pointers in general stress me out.

Fair enough, casting to and from void pointers explicitly throws away the type information.

> The quirks in many other languages can be rough, but I feel like Cs leave developers in a scarier place given the risk factors

The two quirks you complained about here are in other languages.

While C has problems, there is not much in this list that is problematic.


Not the OP, but:

* the comma operator (why not just allow blocks to be used as expressions?)

* multi-character constants (yay implementation-defined behavier)

* interlacing syntactic constructs (why?????)

* the fact that the --> operator works

* the idx[arr] thing

* the fact that you need a dirty hack with enums to check something at compile time

* flat initializer lists (why?)

* void pointers (instead of a proper type system)

* the syntax for function types

* the fact that X-Macros are needed


> * multi-character constants (yay implementation-defined behavier)

Isn't all of reference-implementation language like Rust and Python all implementation-defined?

> * the fact that the --> operator works

I pointed out elsewhere (with a link) that that specific construct works in most popular languages, like Java.

> * the fact that you need a dirty hack with enums to check something at compile time

It's not a check, its an assert. The alternative, for languages like Java and C#, is not having compile-time asserts at all.

> * the fact that X-Macros are needed

They are not needed, and indeed, equivalent functionality isn't in most popular languages (or anything but, Go, I think).

Since many of your complaints are applicable to other languages, it seems, to me anyway, that what you know of C is what you've read online in popular forums.


Implementation-defined is not a problem if there is only one implementation, since your code is going to work the same everywhere.

Languages like Java literally copied most of their syntax from C, so obviously they have the same problem and I hate them too.

Ditto for other languages that don't have compile-time asserts.

X-macros are a poor substitute for… well, actual macros. Or just metaprogramming in general.


The trick for preserving array lengths in function signatures looks quite useful (e.g., `void foo(int p[static 1]);` for a non-null pointer p). Unfortunately, I think the overloaded use of `const` and `static` somewhat obfuscates its semantics.


Casting to and from void is a "lesser known" feature of C?


Also “lesser known” means “not in C FAQ” to me. Half or more of TFA is a pretty regular C FAQ material that everyone C already has read. I mean, sure, right?


Pretty much.

Although even things in the C FAQ get overlooked and forgotten.

eg: a 'Day of the Week' one liner such as

dow = (y + y/4 - y/100 + y/400 + m["-bed=pen+mad."] + d) % 7;

with int day (d), month (m), year (y) inputs and single int day of week output has a slight twist that causes a few head scratches for some.


That... that way to handle leap years can't possibly be right, can it? It doesn't account that you don't need to add the leap day of the current year if the day is before Feb 29


Err, my bad, I left out the prequel (still a one liner)

y -= m < 3; dow = as above, etc;

I was focused on an 'obscure' yet fundemental part of the C language pointer | array equivalence.

Addendum: https://c-faq.com/misc/zeller.html


* Context: const and compile-time constants

Constructs fully determined at compile time have some benefits. But const in C is weaker than constexpr that C++ has.

As prog-fh summarizes on https://stackoverflow.com/questions/66144082/why-does-gcc-cl...

> "The const means « I swear that I won't change the value of this variable » (or the compiler will remind me!). But it does not mean that it could not change by another mean I don't see in this compilation unit; thus this is not exactly a constant."

One example of invalid C:

const int mysize = 2; const int myarray[mysize];

gcc: error: variably modified ‘myarray’ at file scope

clang: warning: variable length array folded to constant array as an extension [-Wgnu-folding-constant] const int myarray[mysize];

* Good news: C can do compile time constant structs and array with deep self-references.

Yes, in C you can define and fully declare complex data structures that are accepted as compile-time constants, including pointers to parts of itself.

See "self-contained, statically allocated, totally const data structure with backward and forward references (pointers)?" for a previous example at https://stackoverflow.com/questions/47037701/can-c-syntax-de...

-----------------

I used this for a game on a retro machine where such a data structure avoids code which would have been several times (perhaps 10 times) bigger: https://github.com/cpcitor/color-flood-for-amstrad-cpc/blob/...

Here's another take showing two variants: where overall construct is an array then a struct: https://gist.github.com/fidergo-stephane-gourichon/792c194e1...


%n format specifier was lesser known until an IOCCC winner made it famous.

https://news.ycombinator.com/item?id=23445546 - Tic-Tac-Toe in a single call to printf


I think it is more like format string attacks made it famous.


Endless CVEs


> I did a sloppy job of gathering some of them in this post (in no particular order) with even sloppier short explanations

I wonder why developers tend to be so self deprecating


I think it’s in part a preemptive defense against the endless nitpicking from their audience.


Yeah, I think it's this. IIRC one of the first places it was posted is on the C_Programming reddit, where the bar for "lesser known" is quite high.


I assume it's just that being self-deprecating or humble correlates with many traits that make you a good developer, so people with those traits are more likely to end up in this career path and stick around in it.

Just like being a sales person doesn't automatically make you overconfident, but being overconfident makes you a good sales person.


Being afraid of failure and the impostor syndrome. You mark the territory and lower the expectations to look better in the end (even if only in your own eyes). A ton of people do it, it's hard to get out of it.


The earlier comments were so cynical I felt like I needed to offer another possibility: maybe they set out to do this in a more systematic way, then got so deep in the weeds they realized it would take them forever to put it into more systematic form, but they didn’t want to just leave it sitting there sight unseen. So they acknowledge it’s sloppier than they would like it to be, but hey, at least it’s something. That’s not really self deprecating so much as just… being transparent?


For me it is what its written there: doing a less than satisfactory job, and not wanting to do it correctly.


I learned a few new things, but it would be more helpful if this had info on whether these are standard and, if so, which standard they are a part of.


> Compound literals are lvalues

> ((struct Foo){}).x = 4;

Do such lvalues have any real use?


You could pass them to a function where something non-null is required but you don't want to use it, like : `f(&(struct Foo){0})`


The key thing about it being an lvalue is that you can take its address — you can only take the address of lvalues. Other than that, no, no real use.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: