Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
__VA_OPT__ Minutiae (corsix.org)
59 points by ingve on Feb 4, 2024 | hide | past | favorite | 6 comments


That recursive macro approach allows FOR_EACH to accept up to 4^N arguments (where N is the number of lines in the definition)… at the cost of making every use of FOR_EACH perform O(4^N) expansions, a fixed number regardless of the actual number of arguments. For high N that can slow down preprocessing.

In the past I've used a style like

    #define FOR_EACH(macro, ...) __VA_OPT__(FOR_EACH1(macro, __VA_ARGS__))
    #define FOR_EACH1(macro, arg, ...) macro(arg) __VA_OPT__(FOR_EACH2(macro, __VA_ARGS__))
    #define FOR_EACH2(macro, arg, ...) macro(arg) __VA_OPT__(FOR_EACH3(macro, __VA_ARGS__))
    #define FOR_EACH3(macro, arg, ...) macro(arg) __VA_OPT__(FOR_EACH4(macro, __VA_ARGS__))
    [etc...]
which doesn't provide exponential growth in the maximum argument count, but does decouple the maximum count from the number of expansions. Even if you continue the pattern up to FOR_EACH100, calling FOR_EACH with 3 arguments will still only do 3+1 expansions.

The author chose N=4 for the recursive macro approach, supporting up to 256 arguments, at the cost of performing 342 expansions for each use. That sounds reasonable; preprocessing is much faster than other compiler passes and you probably won't notice the cost at all. So it's probably better than my approach.

On the other hand, if I bump the recursive approach up to N=10, supporting up to 1048576 arguments at the cost of performing around that many expansions per use, then performance becomes obviously unacceptable (when running `clang -E`, you can watch the tokens being emitted one by one). So you do have to set a limit somewhere.

I wonder if there is some way to get the best of both worlds – exponential maximum number of arguments per line of code, but still only a linear number of expansions per actual argument.


(Grumpy old man edit: __VA_OPT__ has been implemented in macro libraries by users for decades without language support. Making it a keyword made no more sense to me than if vector was made a language primitive in C++)

Tree-based expansion machines are the usual way to do preprocessor recursion for non-trivial applications (see: order-pp, chaos-pp). They have that desirable property of letting you process an exponential number of tokens using a linear number of macros, while letting you “bail out” at any time and avoid needing to resolve all of those deeply-nested macro expansions when there isn’t more work to be done.

Getting the linear->exponential property is actually the easy part, since you just need two parallel hierarchies of macros (shamelessly plugging https:.//github.com/notfoundry/pp2/blob/master/include/pp2/machine/vm.h as the most readable implementation). It’s how you stop expanding before the heat death of the universe once you have an effectively unlimited number of recursive expansions available that’s really interesting: if you design all of your recursive macros to return their output wrapped in parentheses, you can write your recursion machine in such a way that at the end of every recursion step, those parentheses are used to complete a function-like macro expansion to get to each next recursion step downstream. When you want to stop computation, you make your recursive macro stop expanding to something parenthesized, and the system naturally stops making progress. In a sense, each recursive macro provide its own potential energy to power the whole recursion system!


Very nice, I had no idea that we finally had recursive macros using this feature.

In practice I find that usually empty __VA_ARGS__ can be tamed by making last non-optional parameter part of the vararg. Sometimes a helper function wrapper is needed though.


You still don't have actual recursive macros, though the semantics of `__VA_OPT__` makes it easier to force expansion and thus rescanning without a lot of additional macro declarations.


One minor point I've recently come across: GCC's old ## trick only works if preceded by a comma. This sometimes requires creating dummy macro arguments, if the optional argument pack needs to go at the front. The C23 way is more useful here.

Anyway, I got this working, for hygienic (shadow-proof) single-expansion macros:

  #define MIN(a_, b_) \
  ({ \
      /* auto a = (a_), b = (b_); ... except safe if the a_ or b_ expressions contain external a or b variables */ \
      CLEANSE_MACRO_VARS((a, a_), (b, b_)); \
      a < b ? a : b; \
  })
Only the ({}) still requires a GNU extension; a `do-while` approach would require passing the output reference in as well, which is ugly.


The GNU preprocessor fixed all these issues decades ago. It provided variadic macros in an elegant way, with a solution for the comma problem.

ISO C and C++ standardization went its own way, inventing grotesque nonsense like __VA_ARGS__ and later __VA_OPT__.




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

Search: