Trigger warning: This post will show some examples that “just work” and “just work fine” but may not be correct. I am posting this because I’d like to hear how you do this. Show us all a much better way.
I was today years old when I “learned” (possibly incorrectly) something (possibly) wrong that I have seen “always” done in the C debug print macros I’ve encountered.
Debug Macros: Basic
To enabled or disable debugging printf output, I often see macros like this:
#if defined(DEBUG_ENABLED)
#define DEBUG_PRINTF(...) printf(__VA_ARGS__)
#else
#define DEBUG_PRINTF(...)
#endif
This allows debug printfs to appear in code when debug is enabled or not exist at all when debugging is not enabled:
DEBUG_PRINTF ("Starting up system...\n");
Debug Macros: Intermediate
There is a fancier version of this macro. Instead of just using a #define DEBUG_ENABLED (present means enabled), the code uses a #define set to a number. That number is used to tell when to include the debug print:
#if (DEBUG_LEVEL > 1)
DEBUG_PRINTF ("tlv_parse_ptr (0x%x, %u, 0x%x)\r\n",
(unsigned int)((uintptr_t)p_buf),
buf_size, (unsigned int)(uintptr_t)p_tlv_table);
#endif
If you “#define DEBUG_LEVEL 1”, the above code will not be part of the program. If the DEBUG_LEVEL is defined to be greater than 1, it will.
That also “just works” but makes the source code messier with all those extra “#if (DEBUG_LEVEL > x)” and #endif” lines.
Debug Macros: Advanced
This leads to the even fancier version which builds the level check into the macro itself. Those get used like:
DEBUG_PRINTF (2, "This will only print at level 2 and above.\n");
The downside of that approach is that a #define macro cannot contain preprocessor #if to check another #define macro inside of it. You can NOT do this:
#define DEBUG_PRINTF(level, ...) \
#if (level < DEBUG_LEVEL) \
printf (__VA_ARGS__); \
#endif
Instead, the macro must have actual C code with an “if” and such, which embeds that extra code into your program:
#define DEBUG_PRINTF(level, ...) \
if (level < DEBUG_LEVEL) \
{ \
printf(__VA_ARGS__); \
}
The problem with this is that even with debugging disabled (where you may not want the bulk of printf included in your program) it will still include all that macro C code in your program. With DEBUG_LEVEL enabled:
#define DEBUG_LEVEL 1
DEBUG_PRINTF(0, "Starting system up...\n")
…you get:
if (0 < 1)
{
printf ("Starting system up...\n");
}
But if you do not want debugging, and set the debug level to 0, you’d get this:
#define DEBUG_LEVEL 0
DEBUG_PRINTF(0, "Starting system up...\n")
if (0 < 0)
{
printf ("Starting system up...\n");
}
That is more like using some syslog() library that is always present, even if you are logging things below the level that go to the system log. But the point of using macros is so you can have the code NOT included at all when you don’t want to use it.
A bit more work is needed, which leads to something like this:
#define DEBUG_LEVEL 1
#if (DEBUG_LEVEL > 0)
#define DEBUG_PRINTF(level, ...) \
if (level < DEBUG_LEVEL) \
{ \
printf(__VA_ARGS__); \
}
#else
#define DEBUG_PRINTF(level, ...)
#endif
Now we get what we want when we want it, and get nothing when we don’t.
But that’s really not important to this story…
The above macros are things that “just work” which may explain why I see things like that often.
But today I learned they are actually not fine. Our good robot friend Copilot “taught” me that the macro above should really look like this, with the code wrapped in something like a do/while:
#define DEBUG_LEVEL 1
#if (DEBUG_LEVEL > 0)
#define DEBUG_PRINTF(level, ...) \
do { \
if (level < DEBUG_LEVEL) \
{ \
printf(__VA_ARGS__); \
} \
} while (0)
#else
#define DEBUG_PRINTF(level, ...) \
do { } while (0)
#endif
I found it odd that it suggested an empty “do / while(0)” and had to research this a bit. You may already know this, but since I had never seen it, I was unaware of the issue(s). Here is what Copilot says about this:
- It forces the macro to behave like ONE statement
- It makes the trailing semicolon safe
- It prevents `if/else` breakage
- It prevents partial execution
- It compiles away to nothing when disabled
I could argue with the robot about #5 since I have certainly used compiler that where happy to leave in unused variables and functions without even a warning about them. ;-)
Of that list, #3 is really the only oneI think that applies to this specific debug macro. As Copilot points out, this example will not work with the non-do/while version of the macro:
if (flag)
DEBUG_PRINTF(1, "msg");
else
do_other_thing();
That would translate into this:
if (flag)
if (level < DEBUG_LEVEL) \
{ \
printf(__VA_ARGS__); \
}
;
else
do_other_thing();
And that won’t compile.
However, the coding standards where I have worked all forbid if/else usage like that. Without having curly braces around the statements, someone might later introduce a bug like this:
if (flag)
DEBUG_PRINTF(1, "msg");
else
do_other_thing();
this_will_always_run();
…because without the braces, the compiler is basically seeing the code like this:
if (flag) DEBUG_PRINTF(1, "msg"); else do_other_thing();
this_will_always_run();
The curly braces ensure the code runs the statements as intended.
If you use that original “bad” macro with the same code but add braces:
if (flag)
{
DEBUG_PRINTF(1, "msg");
}
else
{
do_other_thing();
}
It looks like this, and works fine:
if (flag)
{
if (level < DEBUG_LEVEL) \
{ \
printf(__VA_ARGS__); \
}
;
}
else
{
do_other_thing();
}
But, the addition of the “useless” do/while loop (or similar logic) around the macro would make it work with the non-curly brace version:
if (flag)
do { \
if (level < DEBUG_LEVEL) \
{ \
printf(__VA_ARGS__); \
} \
} while (0)
;
else
do_other_thing();
Try it yourself.
Here is a sample you can test in a web browser:
https://onlinegdb.com/KpTEGVsg7
Or, here is the code:
#include <stdbool.h>
#include <stdio.h>
#define DEBUG_LEVEL 1
#if (DEBUG_LEVEL > 0)
#define DEBUG_PRINTF(level, ...) \
do { \
if (level < DEBUG_LEVEL) \
{ \
printf(__VA_ARGS__); \
} \
} while (0)
#else
#define DEBUG_PRINTF(level, ...) \
do { } while (0)
#endif
void do_other_thing () { return; }
int main()
{
bool flag = true;
if (flag)
DEBUG_PRINTF (0, "Hello\n");
else
do_other_thing();
return 0;
}
Here’s where you come in…
What have you seen? I know modern PC/Linux/Mac environments don’t care about a few extra K of code space, so far fancier debug logging is likely common. But for embedded space, have you ran into the macros like I have, or do you always see the “do / while(0)” ones or something else?
Comment away…
Until next time…

Any macro you need to behave like an proper statement really should use something like the “do { } while (0)” thing to avoid things breaking unexpectedly in some corner case you didn’t consider in your coding standards or whatever. It has the added advantage that you can prevent multiple evaluation of macro arguments, too, since it introduces a block where you can define some temporary variables. Something like “do { int v1___ = (arg1); int v2___ = (arg2); /* do stuff with v1___ and v2___ */ } while (0)”. The trailing underscores are to minimize the chance that the temporaries collide with any variables referenced in the macro arguments.
On the other hand, if you need a macro to behave like a function call, you can’t use that trick since you obviously can’t put a do loop inside an expression.