Using a Macro to Check Whether Another Macro Is Defined

C/C++ preprocessing

Posted by Bruce Lee on 2024-03-23

About Me

Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.

Main Topics

  • Engineering Projects: Exploring implementation details and how technical systems work.
  • C/C++: Notes on language features and programming techniques.
  • The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.

For more, visit the categories page.

Contact

If you have questions or would like to discuss something, please get in touch through the About page.

Thank you for reading and for your support. I hope these notes help you on your own technical journey!


Using a Macro to Check Whether Another Macro Is Defined

The original question is on Stack Overflow:

https://stackoverflow.com/questions/26099745/test-if-preprocessor-symbol-is-defined-inside-macro

Consider this definition:

1
#define TRACE(x) "" #x

#x uses the C preprocessor’s stringizing operator to turn the argument’s tokens into a string literal. For example:

1
printf(TRACE(this_macro));

This prints this_macro.

Why Put an Empty String Before #x?

Compare these two definitions:

1
2
#define TRACE2(x) #x
#define TRACE3(x) "" #x

I asked several AI systems what difference the two forms made. Their common claim was that the results were usually the same but differed in a few unusual cases.

Claude claimed that TRACE3(x) was safer because it concatenated multiple macro arguments into one string, whereas TRACE2(x) did not. Gemini 1.5 Pro offered this example:

1
printf(TRACE3(hello) TRACE3(world));

It claimed that TRACE3 could join the strings while TRACE2 could not. GPT-4 suggested that TRACE3 would still produce an empty string for an invalid argument and was therefore safer.

I initially accepted these explanations, but wrote macro1.c to test them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
//#x会将操作数转变为字符串字面量
#define TRACE(x) printf("Value of " #x " is %d\n", x)

#define TRACE2(x) #x
#define TRACE3(x) "" #x
int main()
{
int a = 32;
//测试#x确实可以将x转变为字符串
TRACE(a);
printf(TRACE2(sd));
printf(TRACE3(TT3));
//测试2对于x本身就是字符串的处理情况
printf(TRACE2(HELLO));
printf(TRACE2("WORLD"));
printf("\n");
//测试在统一printf函数中连续的两个宏参数连接情况
printf(TRACE2(HELLO) TRACE2(WORLD));
printf(TRACE3(HELLO) TRACE3(WORLD));
printf("\n");
//测试3对于x本身就是字符串的处理情况
printf(TRACE3(HELLO));
printf(TRACE3("WORLD"));
//测试非法x参数
printf(TRACE2());
printf(TRACE3());
return 0;
}

None of the ordinary cases showed a difference. I then tried an empty argument. Following the AI explanations, I expected TRACE2 to produce no string and fail, while TRACE3 would produce an empty string. Instead, both produced the same warning:

1
2
3
4
5
6
7
8
9
10
11
12
13
macro1.c: In function ‘main’:
macro1.c:26:23: warning: zero-length gnu_printf format string [-Wformat-zero-length]
26 | printf(TRACE2());
| ^
macro1.c:4:20: note: in definition of macro ‘TRACE2’
4 | #define TRACE2(x) #x
| ^
macro1.c:6:19: warning: zero-length gnu_printf format string [-Wformat-zero-length]
6 | #define TRACE3(x) "" #x
| ^~
macro1.c:27:16: note: in expansion of macro ‘TRACE3’
27 | printf(TRACE3());
|

To check whether optimization caused the result, I disabled it with CFLAGS=-O0:

1
gcc macro1.c CFLAGS=-O0

The result remained the same. At the time, I had no satisfactory explanation and moved on to the macro’s more interesting use.

The important distinction is that these were claims I tested, not conclusions established by the experiment. Stringizing an empty macro argument already produces an empty string literal; prepending "" is not what makes that case valid. Adjacent string literals are concatenated in either form when they are present.

Testing Whether a Macro Expands

The first answer in the linked discussion defines:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <cstring>

#define TRACE_STRINGIFY(item) "" #item
#define TRACE(macro, message) \
do { \
if (strcmp("" #macro, TRACE_STRINGIFY(macro))) \
std::cout << message << "\n"; \
} while (0)

If a macro is defined to a replacement that changes its spelling, the extra TRACE_STRINGIFY expansion stage expands it before stringizing it. In "" #macro, direct stringizing instead preserves the argument’s original spelling.

The resulting strings differ, so strcmp returns a nonzero value and the print statement runs. If the identifier does not expand, both paths produce the same string. strcmp returns zero and nothing is printed.

This explanation also reveals a limitation: the test does not recognize a self-referential definition such as:

1
#define myself myself

The example above is C++. I adapted it to C in macro2.c.

Adding a Diagnostic

Here is macro2.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#define TRACE_STRINGIFY(item) "" #item
#define TRACE(macro, error_message, ...) \
do { \
if (!strcmp("" #macro, TRACE_STRINGIFY(macro))) \
fprintf(stderr, "[ERROR]:[TRACE] (%s:%d) " error_message "\n", __FILE__, __LINE__, ##__VA_ARGS__); \
} while (0)

#define dont_know_exist 52
//real_dont_exist
int main()
{

TRACE(dont_know_exist, "This \"%s\" macro don't exist", "dont_know_exist");
TRACE(real_dont_exist, "This \"%s\" macro don't exist", "real_dont_exist");
return 0;
}

I changed TRACE to report the case where the macro does not expand. The logical change is simply negating the result of strcmp.

A variadic argument list supplies the diagnostic message to fprintf, which writes to stderr. The predefined macros __FILE__ and __LINE__ identify the call site.

This can be useful for reporting an expected macro that is missing in a project without a configuration system. The original notes also considered how such checks relate to configuration-driven builds using tools such as Kconfig. However, this string-comparison technique is a runtime expansion test, with the limitation described above; it does not replace #if or #ifdef when the preprocessor must actually include or exclude source code.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !