2

I am trying to define a debug_log function in a preprocessor #define in order for this function to exist only in Debug mode. The thing is that I wish to use a variable_argument function :

#ifdef DEBUG
    #define DEBUG_ENABLED 1
#else
    #define DEBUG_ENABLED 0
#endif

#define debug_log(msg, ...)                                                                 \
        do {                                                                                \
            if (DEBUG_ENABLED) {                                                            \
                char str[300];                                                              \
                int length = -1;                                                            \
                va_list argList;                                                            \
                va_start( argList, msg );                                                   \
                length = vsnprintf(str, sizeof(str), msg, argList);                         \
                va_end( argList );                                                          \
                if (length > 0)                                                             \
                {                                                                           \
                    fprintf(stderr, "%s, %d ",__func__, __LINE__);                          \
                    fprintf(stderr, "%s", str);                                             \
                    fprintf(stderr,"\n");                                                   \
                }                                                                           \
            }                                                                               \
        } while (0)                                                                         \

The compiler is returning :

error: ‘va_start’ used in function with fixed args [build] 20 | va_start( argList, msg ); \

Thank you for your help ;)

Amaury
  • 73
  • 7
  • 1
    Just place the function definition between preprocessor statements `#if DEBUG_ENABLED == 1` and `#endif`. – Weather Vane Jul 27 '20 at 13:46
  • I don't think this has anything to do with the preprocessor. – Scott Hunter Jul 27 '20 at 13:47
  • 1
    Why not create a macro calling a printing *function* based on defined symbol? – Eugene Sh. Jul 27 '20 at 13:51
  • 1
    See also [`#define` macro for debug printing in C?](https://stackoverflow.com/questions/1644868/define-macro-for-debug-printing-in-c/1644898#1644898). – Jonathan Leffler Jul 27 '20 at 14:12
  • @EugeneSh. This is an alternative but it is forcing me to call something like #ifdef DEBUG debug_log(...) #endif everytime I call want to print the log. In fine I want only debug_log(...) – Amaury Jul 27 '20 at 14:42

2 Answers2

4

You're not actually defining a function here. You're defining a block of code that expects to be part of a variadic function. You're most likely not in a variadic function when you call this macro, hence the error.

Instead, define an actual function inside of an #if block, along with a dummy function-like macro in the #else block that does nothing.

#ifdef DEBUG

#define debug_log(...)  debug_log_impl(__func__, __LINE__, __VA_ARGS__)

void debug_log_impl(const char *func, int line, const char *msg, ...)
{
    char str[300];
    int length = -1;
    va_list argList;
    va_start( argList, msg );
    length = vsnprintf(str, sizeof(str), msg, argList);
    va_end( argList );
    if (length > 0)
    {
        fprintf(stderr, "%s, %d ", func, line);
        fprintf(stderr, "%s", str);
        fprintf(stderr,"\n");
    }
}

#else

#define debug_log(...) (void)0

#fi

There was a question in the comments regarding why (void)0 should be used in the #else case instead of an empty expression. Suppose you were to use debug_log as the left operand of the comma operator:

while (debug_log("iterating, x=%d",x), x>0)

With the definition above, if DEBUG is not defined this line expands to:

while ((void)0, x>0)

If it were an empty expression, it would expand to:

while (, x>0)

Which is a syntax error.

dbush
  • 205,898
  • 23
  • 218
  • 273
  • What is the compiler behaviour to a (void)0 instruction ? Is there an equivalent instruction in assembly or is it totally ignored ? – Amaury Jul 27 '20 at 14:47
  • Most likely, this would be in a header file where the external function definition would be inappropriate. It could be made `static inline`, or the macro could call an external function with variable arguments. It actually needs to be a macro because the `__func__` and `__LINE__` would not expand to the correct thing if it was a function. – Ian Abbott Jul 27 '20 at 14:50
  • 1
    @Amaury This creates an expression with the same return type as the function. That way it can be dropped in place anywhere the real function would otherwise be called. As it is a statement expression with no side effects, compilers will typically optimize it away. – dbush Jul 27 '20 at 14:50
  • @Amaury, given that the type involved is `void`, perhaps the more interesting question is why `(void)0` is used instead of nothing at all. Indeed, I can't think of any *semantic* difference that could arise if that macro is used in a place where the debug version is permitted, but some compilers warn about empty statements, and `(void)0;` is a non-empty statement. – John Bollinger Jul 27 '20 at 14:56
  • @JohnBollinger See my edit for an example of where an empty expression would cause a problem. – dbush Jul 27 '20 at 15:03
  • 1
    @JohnBollinger The cast to `(void)` usually suppresses warnings about expressions that have no effect. – Ian Abbott Jul 27 '20 at 15:09
  • `fprintf(stderr, "%s, %d ",__func__, __LINE__);` isn't very using in an actual function as it would always be the same. Nevermind the evil of using `stderr` for logging purposes... – Andrew Henle Jul 27 '20 at 15:09
  • Yes, @IanAbbott, that's part of my point. The contrast is between non-empty statements `(void)0;` and empty statements `;` that would arise from use of an alternative macro definition with empty replacement text. – John Bollinger Jul 27 '20 at 15:11
  • I see your point, @dbush. So yes, there is a context where it actually would make a semantic difference. – John Bollinger Jul 27 '20 at 15:15
  • Thanks all for your help, it's working pretty well ! Have a great day ;) – Amaury Jul 27 '20 at 15:20
  • @Armaury Are you are getting the calling function name and line number in the debug output? – Ian Abbott Jul 27 '20 at 15:26
  • I made an update to account for the function/line number. – dbush Jul 27 '20 at 15:30
  • Either the `va_start` needs to refer to `line` now, or you need to rearrange the parameters (e.g. put `msg` after `func` and `line`). – Ian Abbott Jul 27 '20 at 15:39
  • @IanAbbott Good catch. Fixed. – dbush Jul 27 '20 at 15:40
  • @IanAbbott Yep, I am having the line number of the call to debug_log(). – Amaury Jul 27 '20 at 15:50
  • 1
    @Amaury See my edit. I made some changes so you should get the correct file/line#. – dbush Jul 27 '20 at 15:51
2

You are mixing up variable arguments for macros with variable argument access for functions.

The ... in the macro parameter list represents the variable arguments of the macro. The identifier __VA_ARGS__ in the macro replacement text expands to the variable arguments.

The ... in a function parameter list represents the variable arguments of the function. On object of type va_list can be used to access these variable arguments using the va_start, va_arg and va_end macros (and perhaps the va_copy macro if needed). Those are defined by #include <stdarg.h>.

Your debug_log macro is not a function, so it is not a function with variable arguments, so it cannot use va_start etc. to access those arguments. What it can do is pass the macros variable arguments as a whole to something else. For your example, snprintf would be a good choice as a replacement for the vsnprintf you orignally used:

#define debug_log(msg, ...)                                                                 \
        do {                                                                                \
            if (DEBUG_ENABLED) {                                                            \
                char str[300];                                                              \
                int length = -1;                                                            \
                length = snprintf(str, sizeof(str), msg, __VA_ARGS__);                      \
                if (length > 0)                                                             \
                {                                                                           \
                    fprintf(stderr, "%s, %d ",__func__, __LINE__);                          \
                    fprintf(stderr, "%s", str);                                             \
                    fprintf(stderr,"\n");                                                   \
                }                                                                           \
            }                                                                               \
        } while (0) 

The above form requires at least two arguments, so you couldn't use it to print a simple debug message such as "got here". As a workaround, the macro can be defined with no fixed parameters, omitting the msg parameter:

#define debug_log(...)                                                                      \
        do {                                                                                \
            if (DEBUG_ENABLED) {                                                            \
                char str[300];                                                              \
                int length = -1;                                                            \
                length = snprintf(str, sizeof(str), __VA_ARGS__);                           \
                if (length > 0)                                                             \
                {                                                                           \
                    fprintf(stderr, "%s, %d ",__func__, __LINE__);                          \
                    fprintf(stderr, "%s", str);                                             \
                    fprintf(stderr,"\n");                                                   \
                }                                                                           \
            }                                                                               \
        } while (0) 

That would allow you to attempt to call the macro with no parameters at all, such as debug_log();, but that will result in a compiler error at the call to snprintf.

Ian Abbott
  • 15,083
  • 19
  • 33
  • Important point !! With your method it is better to add ##__VA_ARGS__ instead of __VA_ARGS__. Otherwise when no additionnal argument is passed, the compiler will append a coma for example snprintf will look like : snprintf(str, sizeof(str), msg, ); This will not compile. – Amaury Jul 27 '20 at 15:48
  • 1
    @Amaury I was trying to avoid GCC extensions as much as possible and the `## __VA_ARGS__` is a GCC extension. That's why I added a second version of the macro where the initial part of the message is to be included within the `...`/`__VA_ARGS__`. – Ian Abbott Jul 27 '20 at 16:14