I am using some logging macros, which are supposed to print out the information provided by the __PRETTY_FUNCTION__
macro and if needed name and value of up to two arguments.
A simplified version of my code looks like
template<typename Value1, typename Value2>
void Log(std::string const& function,
std::string const& variable_1 = "", Value1 value_1 = Value1(0),
std::string const& variable_2 = "", Value2 value_2 = Value2(0)) {
std::cout << function << " "
<< variable_1 << " " << value_1 << " "
<< variable_2 << " " << value_2 << std::endl;
}
#define LOG0() Log(__PRETTY_FUNCTION__)
#define VARIABLE(value) #value, value
#define LOG1(value) Log(__PRETTY_FUNCTION__, VARIABLE(value))
#define LOG2(value, value1) Log(__PRETTY_FUNCTION__, VARIABLE(value), VARIABLE(value1))
#define LOG(arg0, arg1, arg2, arg, ...) arg
#define CHOOSE(...) LOG(,##__VA_ARGS__, LOG2, LOG1, LOG0)
#define Debug(...) CHOOSE(__VA_ARGS__)(__VA_ARGS__)
I can use these macros like
Debug();
int x = 0;
Debug(x);
int y = 1;
Debug(x, y);
When I compile this code with clang I get a nice output containing class and function information as well as name and value of the variables. But I also get the warning that standard compliant code is not allowed to have zero variadic arguments.
warning: token pasting of ',' and __VA_ARGS__ is a GNU extension [-Wgnu-zero-variadic-macro-arguments]
#define CHOOSE(...) LOG(,##__VA_ARGS__, LOG2, LOG1, LOG0)
^
warning: must specify at least one argument for '...' parameter of variadic macro [-Wgnu-zero-variadic-macro-arguments]
Debug();
Gcc on the other hand fails to compile with
error: expected primary-expression before ‘)’ token
#define LOG1(value) Log(__PRETTY_FUNCTION__, VARIABLE(value))
^
Debug();
Obviously it is dangerous to work with zero variadic arguments.
The hard part of this is distinguishing between Debug()
and Debug(x)
. In both cases, you are technically passing a single argument to the macro Debug
. In the first case, the token sequence of that argument is empty, and in the second case it contains a single token. These cases can be distinguished with a trick due to to Jens Gustedt.
Here's the trick:
#define COMMA_IF_PARENS(...) ,
Observe that COMMA_IF_PARENS X
produces a comma if X
starts with (...)
, and otherwise expands to a token sequence containing no additional (top-level) commas. Likewise, COMMA_IF_PARENS X ()
produces a comma if X
is empty or starts with (...)
and otherwise expands to a token sequence containing no additional (top-level) commas. (In each case, the token sequence also contains all the top-level commas from X
itself.)
We can use that trick like this:
#define CHOOSE(...) \
LOG(__VA_ARGS__ \
COMMA_IF_PARENS __VA_ARGS__ \
COMMA_IF_PARENS __VA_ARGS__ (), \
CHOICES)
Note that:
COMMA_IF_PARENS __VA_ARGS__
produces the number of commas in __VA_ARGS__
plus 1 if __VA_ARGS__
starts with (...)
.COMMA_IF_PARENS __VA_ARGS__ ()
produces the number of commas in __VA_ARGS__
plus 1 if __VA_ARGS__
is empty or starts with (...)
. (Note that this can fail if __VA_ARGS__
ends in the name of a function-like macro, and we don't address that potential problem here.)Let c be the number of commas in __VA_ARGS__
, p be 1 if __VA_ARGS__
starts with (...)
and 0 otherwise, and e be 1 if __VA_ARGS__
is empty and 0 otherwise.
The number of macro arguments produced prior to CHOICES
is 3 c + 2 p + e. Taken modulo 3, the number of commas is 0 or 2 for a normal argument, and 1 if we have an empty list of arguments.
This gives us 6 cases we care about:
#define CHOICES LOG2, impossible, LOG2, LOG1, LOG0, LOG1
#define LOG(a0, a1, a2, a3, a4, a5, arg, ...) arg
However, this doesn't quite work, because we need to delay expanding the LOG(...)
macro invocation until after we expand the COMMA_IF_PARENS
machinery. One way to do that is:
#define LPAREN (
#define EXPAND(...) __VA_ARGS__
#define CHOOSE(...) EXPAND(LOG LPAREN COMMA_IF_PARENS [...]))
We also should add another comma to the end of CHOICES
so that we always have a (possibly empty) argument corresponding to the ...
parameter of LOG
.
Putting it all together, we get this:
#define COMMA_IF_PARENS(...) ,
#define LPAREN (
#define EXPAND(...) __VA_ARGS__
#define CHOOSE(...) \
EXPAND(LOG LPAREN \
__VA_ARGS__ COMMA_IF_PARENS __VA_ARGS__ COMMA_IF_PARENS __VA_ARGS__ (), \
LOG2, impossible, LOG2, LOG1, LOG0, LOG1, ))
#define LOG(a0, a1, a2, a3, a4, a5, arg, ...) arg
with everything else unchanged from your code. (This can be generalized much further, but the above is sufficient to demonstrate the technique.)