2

There are plenty of questions discussing how to count __VA_ARGS__ and the problem of zero arguments (e.g. [1] and [2]). However, answers to these questions usually are either not portable, since they use the GCC specific ##__VA_ARGS__ to account for the "0 arguments" case, or they are portable, but cannot account for 0 arguments (both COUNT_ARGS() and COUNT_ARGS(something) are evaluated to 1).

Is there a solution that can count the number of arguments in __VA_ARGS__, including 0, that can work in any C compiler complying to the standard?

Luiz Martins
  • 1,644
  • 10
  • 24
  • 1
    This strikes me as a bit of an XY problem: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem (e.g.) For a varargs _function_ (i.e. _callee_) there is no way to know how many args with just that. (e.g.) For `printf`, the format string details the arg count by the number of `%` in it. Or, the use of a _sentinel_ value at the end (e.g. `NULL` if passing pointers). Assuming you _could_ do this, what is the _usage_ for it? I'm sure there's a better way that doesn't involve the massive contortions found in your answer. I _love_ obscure macros but I'd never do this one. – Craig Estey Mar 10 '21 at 00:47
  • @CraigEstey *"there is no way to know how many args with just that"*. I don't think that is true, as my solution (along with [other solutions](https://stackoverflow.com/questions/26682812/argument-counting-macro-with-zero-arguments-for-visualstudio/26685339#26685339) with `##__VA_ARGS__`) can find it. The use can be found [here](https://stackoverflow.com/a/44479664/14656198). If you want to create a "foreach" macro, you need this AFAIK. – Luiz Martins Mar 10 '21 at 01:34
  • Show the usage you intend for this. For the moment, forget about the implementation of the macro. Assume you've the the macro, show the example program that uses it. I'll bet anything you show me, I can come up with a cleaner/alternate solution. In fact, forget CPP altogether. Write a metaprogram [in (e.g.) `perl/python`] that scans [and modifies] the source. The macro is a _solution_ to a problem. What is the problem? It is _not_ "I need to count the number of arguments in a varargs macro"--that is [still] a solution [to a need]. – Craig Estey Mar 10 '21 at 01:39
  • @CraigEstey There are [plenty](https://stackoverflow.com/questions/3563184/variadic-macros-with-zero-arguments-and-commas) of [problems](https://stackoverflow.com/questions/2124339/c-preprocessor-va-args-number-of-arguments) in the same [vein](https://stackoverflow.com/questions/21474061/detect-presence-or-absence-of-arguments-in-a-c-macro) and none have a XY problem. The macro is the point. It just happens that the answers provided did not qualify on criteria that I found important (i.e. compiling in any compiler, and counting independent of the number), so I created a stricter question. – Luiz Martins Mar 10 '21 at 02:43
  • @CraigEstey However, if you are interested in why I need the macro, I want to transform a variadic function-wannabe macro like `foo('a', 1, 1.2)` into `foo('a', CHAR_SIGNATURE, 1, INT_SIGNATURE, 1.2, FLOAT_SIGNATURE)`, independent of the number of arguments passed. Why you may ask? Because I want to dynamically typecheck variadic functions. Why you may ask? Because I'm working on a object oriented system with polymorphic methods in pure C (so metaprogramming will not suffice). Why? cuz I got mad at c++ ;). – Luiz Martins Mar 10 '21 at 02:44
  • @CraigEstey Jokes aside, there are already questions about typechecking variadic functions, so there's no need for me to post another one. The usual answer is [you can't](https://stackoverflow.com/a/6471984/14656198). Of course, that is false, it just requires a lot of nasty looking code and some macro trickery. So, the conversation usually gets shut down with *"why bother, just use c++"*. In any case, [there's no need for another question about that](https://stackoverflow.com/questions/24081085/is-there-a-gcc-w-option-that-would-alert-me-when-im-providing-wrong-argument?noredirect=1&lq=1). – Luiz Martins Mar 10 '21 at 02:52
  • IMO, `c++` is a travesty [and unnecessary]. What you're talking about is metaprogramming. You just assume that `cpp` has to be the metaprogram. Consider using (e.g.) `m4` [a _real_ macro processor--what `autoconf` uses] rather than torturing `cpp`. I've done many such [and much more complex] things in `c` using non-cpp macro processors. – Craig Estey Mar 10 '21 at 03:04
  • BTW, in your example [I assume] `foo` [the macro] has to gen a call to `foo` [the function]. You need the type to precede the value and a sentinel: `foo(CHAR,'a',INT,1,FLT,1.2,END);`. Otherwise, the variadic _function_ doesn't know when to stop and needs to know the type beforehand so you can do: `while (1) { int typ = va_arg(ap, int); if (typ == END) break; switch (typ) { case CHAR: int chr = va_arg(ap, int); break; case INT: int val = va_arg(ap, int); break; case FLT: double flt = va_arg(ap, double); break; } }` – Craig Estey Mar 10 '21 at 03:19
  • @CraigEstey Indeed, I am torturing C and its preprocessor a lot. This is more a hobby project to learn how OO systems work and to see how much one can squeeze out of the (not very powerful) C preprocessor than anything practical to be deployed in the real world. However I'm still interested in the subject, so thank you for the reference, gonna take a look at it. – Luiz Martins Mar 10 '21 at 03:29
  • @CraigEstey Indeed. My plan was to use two sentinels at the end (as in `foo('a',CHAR,1,INT,1.2,FLT,0 \*placeholder*\ ,END)`), but your idea seems better. Thank you for the suggestion. – Luiz Martins Mar 10 '21 at 03:33
  • Also, in your example, the `cpp` macro will have a hard time translating: `foo('a', 1, 1.2)`. How do you get it to figure out that `a` --> `CHAR,'a'` This is easy in `m4` [I use `perl`] because to do the translation you need (crudely) `$typ = "CHAR" if ($arg =~ /'/); $typ = "FLT" if ($arg =~ /[.]/); printf("%s,%s",$typ,$arg);` – Craig Estey Mar 10 '21 at 03:36
  • You _must_ put the type first because `double` is 8 bytes and others are 4 bytes. `va_arg` must have the C type/cast. On x86 arches [non-float args are passed in general purpose regs but float values are passed in float regs (e.g. `xmm`), up to six args and then pushed to the stack [4 args for WinX]. All the other types are `int` equiv but `double` is the outlier. And, what about `STR,"hello world"`? Add that to the mix [for fun] and make it work and you'll see what I mean – Craig Estey Mar 10 '21 at 03:42
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/229709/discussion-between-luiz-martins-and-craig-estey). – Luiz Martins Mar 10 '21 at 03:44

1 Answers1

4

After some research, I found a blog post by Jens Gustedt named "Detect empty macro arguments" (which I found as a comment by him in this answer). Together with the counting solution found in another answer by H Walters (which is similar to this one) we can build a solution that should work in any C99 compiler. The code below is a unification of these two methods.

One noteworthy change I made was adding extra EXPAND macros. As discussed in this question, MSVC does not expand __VA_ARGS__ like most other compilers, so an extra step of expansion is necessary.

/* NOTE: In these macros, "1" means true, and "0" means false. */

#define EXPAND(x) x

#define _GLUE(X,Y) X##Y
#define GLUE(X,Y) _GLUE(X,Y)

/* Returns the 100th argument. */
#define _ARG_100(_,\
   _100,_99,_98,_97,_96,_95,_94,_93,_92,_91,_90,_89,_88,_87,_86,_85,_84,_83,_82,_81, \
   _80,_79,_78,_77,_76,_75,_74,_73,_72,_71,_70,_69,_68,_67,_66,_65,_64,_63,_62,_61, \
   _60,_59,_58,_57,_56,_55,_54,_53,_52,_51,_50,_49,_48,_47,_46,_45,_44,_43,_42,_41, \
   _40,_39,_38,_37,_36,_35,_34,_33,_32,_31,_30,_29,_28,_27,_26,_25,_24,_23,_22,_21, \
   _20,_19,_18,_17,_16,_15,_14,_13,_12,_11,_10,_9,_8,_7,_6,_5,_4,_3,_2,X_,...) X_

/* Returns whether __VA_ARGS__ has a comma (up to 100 arguments). */
#define HAS_COMMA(...) EXPAND(_ARG_100(__VA_ARGS__, \
   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, \
   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0))

/* Produces a comma if followed by a parenthesis. */
#define _TRIGGER_PARENTHESIS_(...) ,
#define _PASTE5(_0, _1, _2, _3, _4) _0 ## _1 ## _2 ## _3 ## _4
#define _IS_EMPTY_CASE_0001 ,
/* Returns true if inputs expand to (false, false, false, true) */
#define _IS_EMPTY(_0, _1, _2, _3) HAS_COMMA(_PASTE5(_IS_EMPTY_CASE_, _0, _1, _2, _3))
/* Returns whether __VA_ARGS__ is empty. */
#define IS_EMPTY(...)                                               \
   _IS_EMPTY(                                                       \
      /* Testing for an argument with a comma                       \
         e.g. "ARG1, ARG2", "ARG1, ...", or "," */                  \
      HAS_COMMA(__VA_ARGS__),                                       \
      /* Testing for an argument around parenthesis                 \
         e.g. "(ARG1)", "(...)", or "()" */                         \
      HAS_COMMA(_TRIGGER_PARENTHESIS_ __VA_ARGS__),                 \
      /* Testing for a macro as an argument, which will             \
         expand the parenthesis, possibly generating a comma. */    \
      HAS_COMMA(__VA_ARGS__ (/*empty*/)),                           \
      /* If all previous checks are false, __VA_ARGS__ does not     \
         generate a comma by itself, nor with _TRIGGER_PARENTHESIS_ \
         behind it, nor with () after it.                           \
         Therefore, "_TRIGGER_PARENTHESIS_ __VA_ARGS__ ()"          \
         only generates a comma if __VA_ARGS__ is empty.            \
         So, this tests for an empty __VA_ARGS__ (given the         \
         previous conditionals are false). */                       \
      HAS_COMMA(_TRIGGER_PARENTHESIS_ __VA_ARGS__ (/*empty*/))      \
   )

#define _VAR_COUNT_EMPTY_1(...) 0
#define _VAR_COUNT_EMPTY_0(...) EXPAND(_ARG_100(__VA_ARGS__, \
   100,99,98,97,96,95,94,93,92,91,90,89,88,87,86,85,84,83,82,81, \
   80,79,78,77,76,75,74,73,72,71,70,69,68,67,66,65,64,63,62,61, \
   60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,45,44,43,42,41, \
   40,39,38,37,36,35,34,33,32,31,30,29,28,27,26,25,24,23,22,21, \
   20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1))
#define VAR_COUNT(...) GLUE(_VAR_COUNT_EMPTY_, IS_EMPTY(__VA_ARGS__))(__VA_ARGS__)

These are some example outputs:

#define EATER0(...)
#define EATER1(...) ,
#define EATER2(...) (/*empty*/)
#define EATER3(...) (/*empty*/),
#define EATER4(...) EATER1
#define EATER5(...) EATER2
#define MAC0() ()
#define MAC1(x) ()
#define MACV(...) ()
#define MAC2(x,y) whatever

VAR_COUNT()                         // 0
VAR_COUNT(/*comment*/)              // 0
VAR_COUNT(a)                        // 1
VAR_COUNT(a, b)                     // 2
VAR_COUNT(a, b, c)                  // 3
VAR_COUNT(a, b, c, d)               // 4
VAR_COUNT(a, b, c, d, e)            // 5
VAR_COUNT((a, b, c, d, e))          // 1
VAR_COUNT((void))                   // 1
VAR_COUNT((void), c, d)             // 3
VAR_COUNT((a, b), c, d)             // 3
VAR_COUNT(_TRIGGER_PARENTHESIS_)    // 1
VAR_COUNT(EATER0)                   // 1
VAR_COUNT(EATER1)                   // 1
VAR_COUNT(EATER2)                   // 1
VAR_COUNT(EATER3)                   // 1
VAR_COUNT(EATER4)                   // 1
VAR_COUNT(MAC0)                     // 1
VAR_COUNT(MAC1)                     // 1
VAR_COUNT(MACV)                     // 1

/* This one will fail because MAC2 is not called correctly. */
VAR_COUNT(MAC2)                     // error
/* But only if it's at the end spot. */
VAR_COUNT(MACV, MAC1, MAC2)         // error
VAR_COUNT(MAC2, MAC1, MACV)         // 3

As pointed out by Jens Gustedt in his blog post, this solution has a flaw. Quote:

In fact ISEMPTY should work when it is called with macros as argument that expect 0, 1 or a variable list of arguments. If called with a macro X as an argument that itself expects more than one argument (such as MAC2) the expansion leads to an invalid use of that macro X.

So, if the list passed contains a function-like macro at the end that requires two or more arguments (such as MAC2), VAR_COUNT will fail.

Other than that, I've tested the macros on GCC 9.3.0 and Visual Studio 2019, and it should also work with any C99 (or more recent) compiler.

Any modification that fixes the flaw mentioned above is appreciated.

Luiz Martins
  • 1,644
  • 10
  • 24
  • 6
    That's ingenious and also horrifying. The code should really be in a spoiler box, with a warning that anyone who has just eaten should not click. – Nate Eldredge Mar 10 '21 at 00:37