Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variadic macro without arguments

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.

  1. Is there any way that I can turn this code into standard compliant code without removing the convenience of having just one macro that takes zero to two arguments?
  2. If this is not possible, is there a way to make also gcc compile this code?
like image 453
Jan Hajer Avatar asked Aug 17 '15 10:08

Jan Hajer


People also ask

How do I use Variadic macros?

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.

What is __ Va_args __ C++?

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.

What is __ Va_opt __?

__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.

What is ## in C macro?

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.


1 Answers

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.)

like image 114
Richard Smith Avatar answered Sep 28 '22 08:09

Richard Smith