Search code examples
c++gccmacrosclangvariadic-macros

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?

Solution

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