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.
To use variadic macros, the ellipsis may be specified as the final formal argument in a macro definition, and the replacement identifier __VA_ARGS__ may be used in the definition to insert the extra arguments. __VA_ARGS__ is replaced by all of the arguments that match the ellipsis, including commas between them.
macro expansion possible specifying __VA_ARGS__ The '...' in the parameter list represents the variadic data when the macro is invoked and the __VA_ARGS__ in the expansion represents the variadic data in the expansion of the macro. Variadic data is of the form of 1 or more preprocessor tokens separated by commas.
__VA_OPT__ is a new feature of variadic macros in C++20. It lets you optionally insert tokens depending on if a variadic macro is invoked with additional arguments. An example usage is comma elision in a standardized manner.
The double-number-sign or token-pasting operator (##), which is sometimes called the merging or combining operator, is used in both object-like and function-like macros. It permits separate tokens to be joined into a single token, and therefore, can't be the first or last token in the macro definition.
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With