NHacker Next on Rails
  • top
  • new
  • best
  • ask
  • show
  • jobs
login
GuidelinesFAQListsAPISecurityLegalApply to YCContact

Hacker News on Rails, powered by React on Rails Pro. Open the source on GitHub by ShakaCode.

Commit 06fc0df

Rendered at 19:28:58 GMT+0000 (Coordinated Universal Time) with Rails + React on Rails RSC.

Fun Extending the C Preprocessor (davidpriver.com)

55 points by david2ndaccount 6d ago | 40 comments

teo_zero 3d ago
There are a couple of constructs that you end up re-creating as soon as you need non-trivial preprocessing.

One of those is the two-stage CAT, another one is the NARG the article talks about.

__COUNTER__ is going to be added to C29, the next version of the standard, maybe there's hope for __VA_COUNT__ and __CAT__ too!

0c3ca83 3d ago
Doing non-trivial preprocessing is a mistake. You get slow compilation, hard to debug code, opaque and fragile code, and incredible complexity. If you want metaprogramming, you're better off with C++ it you need C header compatibility, or zig if you don't.

Otherwise, it's probably a good idea to pretend that the only preprocessing directive is #include. With constexpr, even #define is obsolescent.

teo_zero 3d ago
Doing non-trivial anything you slow down compilation, make debugging harder and increase fragility and complexity. Sometimes that's a mistake, sometimes not.

Getting rid of something because someone sometimes abuses it, is short-sighted.

0c3ca83 3d ago
You don't need to get rid of it to leave it unused. Macro wizardry leads to poor code.
jkercher 3d ago
Yes and no. If you want to make something like a generic dynamic array, you can do it with type erasure (`void*`), but then your code is no longer typesafe. I would find that more fragile. The preprocessor allows you to add constructs that make it behave like any other modern language.

As an aside, clangd can dereference a macro in place to make debugging easy.

uecker 3d ago
And for generic data structures one can also a very thin layer of macros around type-erased implementations.
0c3ca83 3d ago
You're usually better off just using realloc, or an abort-on-oom wraper of it, in my experience.
jkercher 2d ago
Okay smarty-pants. What if I want a generic map then ;)
mrpippy 3d ago
> slow compilation, hard to debug code, opaque and fragile code, and incredible complexity

Most people describe C++ in those terms.

0c3ca83 3d ago
Exactly. So if you're willing to accept that downside, C++ does a better job of letting you express that kind of cleverness.
uecker 3d ago
I am not sure. I agree one should minimize macros in C, but for some things a small macro layer still beats the complexity of C++ and is not inferior in my opinion.
0c3ca83 3d ago
C++ templates are far cleaner, easier to debug, and produce more comprehensible errors than C macros. And C++ templates are rightly famous for being terrible at all of the above.
fuhsnn 3d ago
There was a "__VA_TAIL__" WG14 proposal[1] that made it pretty simple to build custom map-like expansions, the OP's map example would be like:

> #define T(x, ...) printf(#x " = %d\n", x); __VA_TAIL__()

> T(1, 2, 3, 4, x, y, z)

You can play with it in the proposal author's ellipsis[2] and my slimcc[3], it's simple enough to implement (only about 30 lines of C) so I kept it in despite not seeing signs of it getting accepted.

[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3307.htm

[2] https://codeberg.org/gustedt/ellipsis

[3] https://github.com/fuhsnn/slimcc

HexDecOctBin 3d ago
Has anyone tried to extend the C Preprocessor to be a fully turing complete language (perhaps something like Snobol) to allow for Terra like meta-programming?

(I am not talking about metalang99 or Cello like stuff, which exploits the Turing tarpit of the currently specified preprocessor but rather tasteful additions to the preprocessor itself).

0c3ca83 3d ago
C++ started as a preprocessor.
cowlevel 3d ago
but not the C preprocessor, but a completely different preprocessor.
HexDecOctBin 1d ago
I asked for tasteful additions :)
tambourine_man 3d ago
So did Objective-C
camel-cdr 3d ago
The C preprocessor is practically turing complete, in the same sense that C it self or your computer is practially turing complete, but not strictly.
david2ndaccount 2d ago
We’ll get there in this series.
raible 2d ago
I use something like this to turn __VA_ARGS__ into count/pointer:

  // Create a stack-allocated argv from N args gcc, clang, tcc, mingw, msvc, etc
  // argc is always correct, for portability argv has a dummy element when argc==0
  // This avoids problematic 0-length compound literal arrays.
  #define _ARG_CV(type, ...)  _ARG_CV1(((type []){0, __VA_ARGS__}))
  #define _ARG_CV1(array)     _ALEN(array) - 1, array + (_ALEN(array) > 1)
  #define _ALEN(array)         sizeof array / sizeof *array
Then it's just:

  #define sum(...) sum(_ARG_CV(int, __VA_ARGS__))
david2ndaccount 2d ago
Right, you can work around it with things like this, but it means you’re forcing the C compiler to parse a compound literal just to get its length, 5 times! when the preprocessor already knows the length of `__VA_ARGS__`. Such contortions when you want a little bit of info that the compiler already has are what cause your compiler times to blow up if you try to do anything sophisticated with the cpp.

Your version expands `sum(1, 2, 3, 4, 5, 6)` to:

    sum(sizeof ((int []){0, 1, 2, 3, 4, 5, 6}) / sizeof *((int []){0, 1, 2, 3, 4, 5, 6}) - 1, ((int []){0, 1, 2, 3, 4, 5, 6}) + (sizeof ((int []){0, 1, 2, 3, 4, 5, 6}) / sizeof *((int []){0, 1, 2, 3, 4, 5, 6}) > 1))
whereas with __VA_COUNT__ it is just:

    sum(6, (int[]){1, 2, 3, 4, 5, 6})
andai 3d ago
It's always bothered me that programming languages aren't programmable.

Well, I keep hearing that Lisp is, but I haven't put much effort into it. (I'd need a Lisp that runs in the browser, for starters!) Maybe I should take another look...

Or maybe I wish the VM was programmable. "Warn me when this type of operation takes place", or "Print the name of every function before it is invoked", etc. I end up just copy-pasting my own temporary statements all over the program.

FuckButtons 3d ago
>(I'd need a Lisp that runs in the browser, for starters!)

We would be in a very different world if Netscape’s exec team had let Brendan Eich just use scheme as he had planned to.

kazinator 3d ago
Block scoped let would have been there from the start, for one thing, rather than nearly two decades of function scoped var.
pajko 4d ago
You don't need to extend the preprocessor to have MAP: https://github.com/swansontec/map-macro
david2ndaccount 4d ago
The article shows how to implement map, but it should be a builtin
fuhsnn 3d ago
A builtin is definitely better for the efficiency alone. Vanilla chibicc needs 5GB to preprocess the example.c from map-macro, in comparison to compiling an amalgamation of itself only takes 50 MB. It of course wasn't optimized for memory use but it's a peak into how much token expansions are involved in the wizardry.
Neywiny 3d ago
But that repo says it's effortful for the preprocessor and you can see in the code it's a recursion limited but what's in the file. If you had, maybe, a few thousand register definitions in an embedded system, that won't do it.
kazinator 2d ago
I would add #defrec which defines macros that allow recursion.
rwmj 3d ago
String comparison in #if would be nice ...
AtalantaDB 3d ago
Wow) I like the idea of making __map a builtin instead of forcing users to simulate recursion through macro expansion. One thing Im curious about is how you handle expansion semantics. Consider: #define X 42 #define STR(x) #x

__map(STR, X)

Should this produce "X" (like STR(X) does), or "42" (if __map expands its arguments first)?

david2ndaccount 2d ago
__map() expands its arguments, like other function-like macros.