The STRINGIFY C preprocessor macro

I wrote my first C programs in 1997 on my father’s old ThinkPad with the DJGPP compiler. Over the past 25 years, I’ve programmed in C intermittently. While I consider myself a proficient C programmer, I can’t claim I am an expert. There are numerous aspects of the C language that I’m not familiar with, the Preprocessor being one of them.

I recently investigated the STRINGIFY double macro trick, which I hadn’t fully grasped before. STRINGIFY converts its argument into a string, it’s usually used to turn __FILE__, __LINE__, or some other constant into a string for logging or debugging. It requires a bit of magic to work properly.

The C preprocessor has a # operator that turns its operand into a string. It’s what the STRINGIFY macro uses. A naive implementation would look something like this:

#define STRINGIFY(x) #x
STRINGIFY(foo)
#define HELLO(x) "Hello " #x
HELLO(world)

In this example, STRINGIFY(foo) expands to "foo", and HELLO(world) expands to "Hello " "world". However, when you pass another macro to STRINGIFY, it behaves unexpectedly. Consider this:

#define STRINGIFY(x) #x
#define FOO bar
STRINGIFY(FOO)
STRINGIFY(__LINE__)

STRINGIFY(FOO) turns into "FOO" instead of "bar", and STRINGIFY(__LINE__) turns into "__LINE__" instead of "4". This happens because STRINGIFY’s argument is stringified before it is expanded, and the preprocessor doesn’t expand strings. How do we fix this? We need another auxiliary macro:

#define STRINGIFY(x) STRINGIFY2(x)
#define STRINGIFY2(x) #x
#define FOO bar
STRINGIFY(FOO)
STRINGIFY(__LINE__)

In this example STRINGIFY(FOO) turns into "bar", and STRINGIFY(__LINE__) turns into "5". The preprocessor expands the macros’ arguments before expanding the macro, unless they are stringified or pasted with other tokens. To get around this limitation, the result of the expansion can be expanded afterward by using an intermediate macro. It’s all about ordering the macro expansion correctly. Here’s the step-by-step process:

  1. STRINGIFY(FOO) is expanded to STRINGIFY2(FOO) because of #define STRINGIFY(x) STRINGIFY2(x)
  2. FOO is expanded to bar with the #define FOO bar macro, because STRINGIFY2 isn’t the # operator. We now have STRINGIFY2(bar)
  3. STRINGIFY2(bar) is correctly expanded to "bar"

This double technique ensures that nested macros are fully expanded before the final stringification occurs, making STRINGIFY work as intended with both simple values and macro expressions.

PS: I’d like to thank Reddit user linukszone for correcting my misunderstanding about macro expansion ordering, and giving the real reason why the macro arguments weren’t expanded.