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__ )
I actually use a macro similar to this in my
cdeclproject.
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 */
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
...
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
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
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_11toARG_101with 101 parameters followed by...returning_101and call it with 991s.
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 )
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 first1becomes the second argument (and so on for the remaining eight1s), and the0becomes the 11th argument that’s returned (false).Similarly,
ARGS_HAS_COMMA(x)with one argument causes__VA_ARGS__to expand intox. This time, thexbecomes the first argument, but everything else is the same, i.e., the0still becomes the 11th argument that’s returned (false).However,
ARGS_HAS_COMMA(x,y)with two arguments causes__VA_ARGS__to expand intox,y. This time,xbecomes the first argument,ybecomes the second argument, the first1becomes the third argument (and so on for the remaining eight1s), and0(false) becomes the 12th argument. The 11th argument is the ninth1that’s returned instead (true).For
ARGS_HAS_COMMA(x,y,z)with three arguments, the eighth1will 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
expandthem 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:
- Two or more arguments.
- An argument within parentheses.
- An argument that is a function-like macro that will expand subsequent parentheses that may generate a comma.
- 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__ () ) \
)
Case 1: Two or More Arguments
This is the easy case we’ve already covered, that is:
ARGS_HAS_COMMA( __VA_ARGS__ )
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) )
that is an argument enclosed within parentheses. This is handled by:
ARGS_HAS_COMMA( ARGS_IS_EMPTY_COMMA __VA_ARGS__ )
where ARGS_IS_EMPTY_COMMA is a helper macro defined as:
#define ARGS_IS_EMPTY_COMMA(...) ,
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:
-
ARGS_IS_EMPTY_COMMAfollowed by(x)will yieldARGS_IS_EMPTY_COMMA(x). - That will expand into
,. - Then
ARGS_HAS_COMMA(,)will yield1(true).
However, if __VA_ARGS__ is empty, then:
-
ARGS_IS_EMPTY_COMMAfollowed by nothing will stay the same. - Not being followed by a
(meansARGS_IS_EMPTY_COMMAwill not expand into,. - Then
ARGS_HAS_COMMA(ARGS_IS_EMPTY_COMMA)will yield0(false).
Case 3: Argument that is a Function-Like Macro
This is to handle cases like:
#define FOO(X) X, 0
that is a function-like macro that expands into something that contains a comma. This is handled by:
ARGS_HAS_COMMA( __VA_ARGS__ () ),
If __VA_ARGS__ is FOO, then:
-
FOOfollowed by()will yieldFOO(x). - That will expand into
X, 0. - Then
ARGS_HAS_COMMA(X, 0)will yield1(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__ () )
If __VA_ARGS__ is empty, then:
-
ARGS_IS_EMPTY_COMMAfollowed by()will yieldARGS_IS_EMPTY_COMMA(). - That will expand into
,. - Then
ARGS_HAS_COMMA(,)will yield1(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
where:
- We use
NAME5to paste togetherARGS_IS_EMPTY_RESULT_and the results from the four cases yielding a token likeARGS_IS_EMPTY_RESULT_xxxxwhere eachxis either0or1. - 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 (all0) and the fourth case is1(true) meaning__VA_ARGS__is empty. - The
ARGS_IS_EMPTY_RESULT_0001in turn expands to a,. - That
,is then passed to the outermostARGS_HAS_COMMAthat returns1(true) only if__VA_ARGS__is empty.
If any of the first three false positive cases return 1 (true), then:
- That would yield an
xxxxthat is not0001, e.g.,0010,0011,1000, etc. - That means the result of
ARGS_IS_EMPTY_RESULT_xxxxdoesn’t expand further. - Which means it doesn’t contain a
,, so the outermostARGS_HAS_COMMAreturns0(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);]
)
- The first test using
x1ensures__VA_OPT__is empty if__VA_ARGS__is. - The second test using
x2ensures__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__ */
The idea is that, instead of the C23/C++20 way of:
__VA_OPT__(,) __VA_ARGS__ // C23/C++20 way
we instead have to do:
VA_OPT( (,), __VA_ARGS__ ) __VA_ARGS__ // our way
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__
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_EMPTYis used to check whether__VA_ARGS__is empty yielding either0or1. - That result is then pasted onto the end of
VA_OPT_EMPTY_via theNAME2helper macro. - If the result is
VA_OPT_EMPTY_0(__VA_ARGS__is not empty), that expands intoSTRIP_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:
- Argument “sliding” as shown in
ARG_11; and: - 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.
Top comments (0)