DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

Using Advanced C Preprocessor Macros for a Pre-C23/C++20 __VA_OPT__ Substitute

Introduction

In _Generic in C, I showed how to implement const function overloading in C via this macro:

#define CONST_OVERLOAD(FN, PTR, ...) \
  STATIC_IF( IS_PTR_TO_CONST(PTR),   \
    const_ ## FN,                    \
    (FN)                             \
  )( (PTR) __VA_OPT__(,) __VA_ARGS__ )
Enter fullscreen mode Exit fullscreen mode

I actually use a macro similar to this in my cdecl project.

This macro makes use of __VA_OPT__ that’s a C23/C++20 feature whereas cdecl requires only C11 at a minimum. Why not just require C23? C23 is still too new and thus not ubiquitous, or many compilers don’t yet default to C23.

That aside, it turns out that many compilers have supported __VA_OPT__ for a while even without C23/C++20. So why is this even an issue? Because it’s not standard and you can’t rely on such support. While this might not keep me up at night, it comes close.

I’ve searched for a long time for a way to simulate __VA_OPT__ using only standard pre-C23/C++20 and found nothing. After continued searching, I’ve finally found all the pieces. To do it requires some “advanced” (sometimes clever, but mostly weird) use of preprocessor macros, so buckle up: it’s going to be a wild ride.

Reminder

As a reminder, what __VA_OPT__ does is fairly simple. In a function-like macro, if the number of arguments given for the variadic parameter is:

  • Greater than zero, that is __VA_ARGS__ will expand into something that is not empty, then __VA_OPT__ will insert the token(s) given to it;

  • Zero, that is __VA_ARGS__ is empty, then __VA_OPT__ will do nothing.

Hence, for the CONST_OVERLOAD() macro, __VA_OPT__(,) will insert a comma to separate the first argument (PTR) from the second argument — but only if there is a second argument.

So, how hard can it be to simulate __VA_OPT__? (Spoiler: it’s harder than it might seem.)

Is There a Comma?

One thing that helps a lot is first to determine whether __VA_ARGS__ contains a comma. For example, given:

#define M(...)  /* doesn't matter */
Enter fullscreen mode Exit fullscreen mode

Then whether __VA_ARGS__ contains a comma or not:

M()             // no arguments: no comma
M(a)            // 1 argument: still no comma
M(a,b)          // 2 arguments: 1 comma
M(a,b,c)        // 3 arguments: 2 commas
...
Enter fullscreen mode Exit fullscreen mode

Hence, we have to distinguish between the 0/1 argument case from the 2+ arguments case. To be explicit for, say, a maximum of 10 arguments, we want a “has comma” macro to return either 0 (false) for “no comma” or 1 (true) for “has comma” as given in this small truth table:

#Args: 0/1 2 3 4 5 6 7 8 9 10
Comma?  0  1 1 1 1 1 1 1 1 1
Enter fullscreen mode Exit fullscreen mode

Given that, we can define ARGS_HAS_COMMA():

#define ARGS_HAS_COMMA(...) \
  ARG_11( __VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 )

#define ARG_11(_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,...) _11
Enter fullscreen mode Exit fullscreen mode

ARGS_HAS_COMMA() uses a helper macro ARG_11() that, given 11 or more arguments (one more than our maximum of 10), always returns its 11th argument.

This implementation self-imposes a maximum of 10 arguments that should be enough for the majority of cases. However, if you want to allow for more, say, 100, change ARG_11 to ARG_101 with 101 parameters followed by ... returning _101 and call it with 99 1s.

What use is always returning the 11th argument? The trick is ARGS_HAS_COMMA() calls ARG_11() supplying its __VA_ARGS__ followed by the above truth table (in reverse). This has the effect of always “sliding” the correct answer into the 11th argument position. For example:

ARGS_HAS_COMMA() => ARG_11( , 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 )
Enter fullscreen mode Exit fullscreen mode

It works as follows:

  • ARGS_HAS_COMMA() with no arguments causes __VA_ARGS__ to expand into nothing. The preprocessor allows empty arguments, so “nothing” becomes the first argument, the first 1 becomes the second argument (and so on for the remaining eight 1s), and the 0 becomes the 11th argument that’s returned (false).

  • Similarly, ARGS_HAS_COMMA(x) with one argument causes __VA_ARGS__ to expand into x. This time, the x becomes the first argument, but everything else is the same, i.e., the 0 still becomes the 11th argument that’s returned (false).

  • However, ARGS_HAS_COMMA(x,y) with two arguments causes __VA_ARGS__ to expand into x,y. This time, x becomes the first argument, y becomes the second argument, the first 1 becomes the third argument (and so on for the remaining eight 1s), and 0 (false) becomes the 12th argument. The 11th argument is the ninth 1 that’s returned instead (true).

  • For ARGS_HAS_COMMA(x,y,z) with three arguments, the eighth 1 will be returned; and so on for up to 10 arguments.

For this and subsequent examples, you can enter the macros into cdecl and have it expand them for you step-by-step to illuminate how the expansions are taking place.

Is __VA_ARGS__ Empty?

To answer that question, there are four cases to consider:

  1. Two or more arguments.
  2. An argument within parentheses.
  3. An argument that is a function-like macro that will expand subsequent parentheses that may generate a comma.
  4. Zero arguments, i.e., __VA_ARGS__ is empty.

The 4th case is the one we’re interested in. So why the first 3 cases? They rule out possible false-positives.

The ARGS_IS_EMPTY() macro is:

#define ARGS_IS_EMPTY(...)                                \
  ARGS_IS_EMPTY_CASES(                                    \
    /*  Case 1: argument with a comma,                    \
        e.g. "ARG1, ARG2", "ARG1, ...", or ",". */        \
    ARGS_HAS_COMMA( __VA_ARGS__ ),                        \
    /*  Case 2: argument within parentheses,              \
        e.g., "(ARG)", "(...)", or "()". */               \
    ARGS_HAS_COMMA( ARGS_IS_EMPTY_COMMA __VA_ARGS__ ),    \
    /*  Case 3: argument that is a macro that will expand \
        the parentheses, possibly generating a comma. */  \
    ARGS_HAS_COMMA( __VA_ARGS__ () ),                     \
    /*  Case 4: __VA_ARGS__ doesn't generate a comma by   \
        itself, nor with ARGS_IS_EMPTY_COMMA behind it,   \
        nor with () after it.                             \
        Therefore, "ARGS_IS_EMPTY_COMMA __VA_ARGS__ ()"   \
        generates a comma only if __VA_ARGS__ is empty.   \
        So this is the empty __VA_ARGS__ case since the   \
        previous cases are false. */                      \
    ARGS_HAS_COMMA( ARGS_IS_EMPTY_COMMA __VA_ARGS__ () )  \
  )
Enter fullscreen mode Exit fullscreen mode

Case 1: Two or More Arguments

This is the easy case we’ve already covered, that is:

ARGS_HAS_COMMA( __VA_ARGS__ )
Enter fullscreen mode Exit fullscreen mode

will return 1 (true) only if two or more arguments are given. Hence, this case must return 0 (false) in order for __VA_ARGS__ to be empty.

Case 2: An Argument within Parentheses

This is to handle cases like:

ARGS_HAS_COMMA( (x) )
Enter fullscreen mode Exit fullscreen mode

that is an argument enclosed within parentheses. This is handled by:

ARGS_HAS_COMMA( ARGS_IS_EMPTY_COMMA __VA_ARGS__ )
Enter fullscreen mode Exit fullscreen mode

where ARGS_IS_EMPTY_COMMA is a helper macro defined as:

#define ARGS_IS_EMPTY_COMMA(...)  ,
Enter fullscreen mode Exit fullscreen mode

As a reminder, the preprocessor will expand a function-like macro only if it’s followed by (. Therefore, if __VA_ARGS__ is something like (x), then:

  1. ARGS_IS_EMPTY_COMMA followed by (x) will yield ARGS_IS_EMPTY_COMMA(x).
  2. That will expand into ,.
  3. Then ARGS_HAS_COMMA(,) will yield 1 (true).

However, if __VA_ARGS__ is empty, then:

  1. ARGS_IS_EMPTY_COMMA followed by nothing will stay the same.
  2. Not being followed by a ( means ARGS_IS_EMPTY_COMMA will not expand into ,.
  3. Then ARGS_HAS_COMMA(ARGS_IS_EMPTY_COMMA) will yield 0 (false).

Case 3: Argument that is a Function-Like Macro

This is to handle cases like:

#define FOO(X)  X, 0
Enter fullscreen mode Exit fullscreen mode

that is a function-like macro that expands into something that contains a comma. This is handled by:

ARGS_HAS_COMMA( __VA_ARGS__ () ),
Enter fullscreen mode Exit fullscreen mode

If __VA_ARGS__ is FOO, then:

  1. FOO followed by () will yield FOO(x).
  2. That will expand into X, 0.
  3. Then ARGS_HAS_COMMA(X, 0) will yield 1 (true).

Case 4: Zero Arguments

The first three cases are to filter out false positives. Finally, case 4 answers the question “Is __VA_ARGS__ empty?” This is handled by:

ARGS_HAS_COMMA( ARGS_IS_EMPTY_COMMA __VA_ARGS__ () )
Enter fullscreen mode Exit fullscreen mode

If __VA_ARGS__ is empty, then:

  1. ARGS_IS_EMPTY_COMMA followed by () will yield ARGS_IS_EMPTY_COMMA().
  2. That will expand into ,.
  3. Then ARGS_HAS_COMMA(,) will yield 1 (true).

Putting it All Together

Given those 4 cases, we use a few helper macros:

#define ARGS_IS_EMPTY_CASES(_1,_2,_3,_4) \
  ARGS_HAS_COMMA( NAME5( ARGS_IS_EMPTY_RESULT_, _1, _2, _3, _4 ) )
#define ARGS_IS_EMPTY_COMMA(...)  ,
#define ARGS_IS_EMPTY_RESULT_0001 ,
#define NAME5(A,B,C,D,E)          NAME5_HELPER( A, B, C, D, E )
#define NAME5_HELPER(A,B,C,D,E)   A ## B ## C ## D ## E
Enter fullscreen mode Exit fullscreen mode

where:

  • We use NAME5 to paste together ARGS_IS_EMPTY_RESULT_ and the results from the four cases yielding a token like ARGS_IS_EMPTY_RESULT_xxxx where each x is either 0 or 1.
  • If the result is ARGS_IS_EMPTY_RESULT_0001, it’s the result we care about, that is the first three cases ruled out the three false positives (all 0) and the fourth case is 1 (true) meaning __VA_ARGS__ is empty.
  • The ARGS_IS_EMPTY_RESULT_0001 in turn expands to a ,.
  • That , is then passed to the outermost ARGS_HAS_COMMA that returns 1 (true) only if __VA_ARGS__ is empty.

If any of the first three false positive cases return 1 (true), then:

  • That would yield an xxxx that is not 0001, e.g., 0010, 0011, 1000, etc.
  • That means the result of ARGS_IS_EMPTY_RESULT_xxxx doesn’t expand further.
  • Which means it doesn’t contain a ,, so the outermost ARGS_HAS_COMMA returns 0 (false) meaning __VA_ARGS__ is not empty.

A __VA_OPT__ Substitute

Now that we’ve defined ARGS_IS_EMPTY, we can use it to implement a __VA_OPT__ substitute when the compiler doesn’t support it.

The first step is to determine whether the compiler does support __VA_OPT__. This can be done by probing the compiler with TRY_COMPILE:

TRY_COMPILE([__VA_OPT__],
  [#define VA_OPT_TEST(...) __VA_OPT__(int) __VA_ARGS__
  ],
  [int x1 = VA_OPT_TEST() 1; VA_OPT_TEST(x2 = 2);]
)
Enter fullscreen mode Exit fullscreen mode
  1. The first test using x1 ensures __VA_OPT__ is empty if __VA_ARGS__ is.
  2. The second test using x2 ensures __VA_OPT__ is not empty when __VA_ARGS__ isn’t.

If both declarations compile successfully, Autoconf will define HAVE___VA_OPT__ in config.h.

Given that, we can conditionally define our own VA_OPT macro based on whether __VA_OPT__ is supported:

#ifdef HAVE___VA_OPT__
# define VA_OPT(TOKENS,...) \
    __VA_OPT__( STRIP_PARENS( TOKENS ) )
#else
# define VA_OPT(TOKENS,...) \
    NAME2( VA_OPT_EMPTY_, ARGS_IS_EMPTY( __VA_ARGS__ ) )( TOKENS, __VA_ARGS__ )

# define VA_OPT_EMPTY_0(TOKENS,...) STRIP_PARENS(TOKENS)
# define VA_OPT_EMPTY_1(TOKENS,...) /* nothing */
#endif /* HAVE___VA_OPT__ */
Enter fullscreen mode Exit fullscreen mode

The idea is that, instead of the C23/C++20 way of:

__VA_OPT__(,) __VA_ARGS__ // C23/C++20 way
Enter fullscreen mode Exit fullscreen mode

we instead have to do:

VA_OPT( (,), __VA_ARGS__ ) __VA_ARGS__  // our way
Enter fullscreen mode Exit fullscreen mode

It’s unfortunately necessary to have to specify __VA_ARGS__ twice since we need the first __VA_ARGS__ to pass to VA_OPT to see whether it’s empty and the second __VA_ARGS__ for the arguments themselves.

If HAVE___VA_OPT__ is defined, we simply define VA_OPT in terms of the real __VA_OPT__ using the helper macro:

#define STRIP_PARENS(ARG)         STRIP_PARENS_HELPER ARG
#define STRIP_PARENS_HELPER(...)  __VA_ARGS__
Enter fullscreen mode Exit fullscreen mode

that strips the outermost () from its arguments. (Try a few examples to convince yourself that it does just that.)

IF HAVE___VA_OPT__ is not defined:

  • ARGS_IS_EMPTY is used to check whether __VA_ARGS__ is empty yielding either 0 or 1.
  • That result is then pasted onto the end of VA_OPT_EMPTY_ via the NAME2 helper macro.
  • If the result is VA_OPT_EMPTY_0 (__VA_ARGS__ is not empty), that expands into STRIP_PARENS(TOKENS) that expands __VA_ARGS__.
  • If the result is VA_OPT_EMPTY_1 (__VA_ARGS__ is empty), that expands into nothing.

Voilà! You now have VA_OPT that can be used in pre-C23/C++20.

Conclusion

As this extended example shows, the C preprocessor has its own weird text processing language inside of C. The two highlighted techniques of:

  1. Argument “sliding” as shown in ARG_11; and:
  2. Conditional macro expansion like ARG __VA_ARGS__ () by inserting __VA_ARGS__ between a macro and (.

are often used in advanced macros, so it’s good to become familiar with them.

References

Top comments (0)