Stringification Operator in C Macros Explained
The stringification operator in the C preprocessor, denoted by a
single hash symbol (#), converts macro parameters into
string literals during the preprocessing phase of compilation. This
article explains how the operator functions, how it handles whitespace
and special characters, why an indirection layer is required to
stringify macro expansions, and common practical applications such as
debugging utilities and custom assertions.
Basic Syntax and Operation
The # operator can only be used within the replacement
list of a function-like macro. When placed immediately before the name
of a formal parameter, the preprocessor wraps the corresponding actual
argument in double quotation marks ("), converting it into
a valid C string literal.
#define TO_STRING(x) #x
const char *str = TO_STRING(Hello World);
// Expands to: const char *str = "Hello World";The conversion happens entirely at compile time, producing an array of characters null-terminated like any standard string literal.
Token Handling, Whitespace, and Escaping
The preprocessor performs specific transformations on the input tokens when applying stringification:
- Whitespace Normalization: Any leading and trailing whitespace around the argument is discarded. Multiple consecutive whitespace characters between tokens inside the argument are condensed into a single space.
- Character Escaping: If the argument contains
characters that have special meaning inside a C string
literal—specifically double quotes (
") and backslashes (\)—the preprocessor automatically inserts an escape character (\) before them.
#define TO_STRING(x) #x
const char *path = TO_STRING(C:\Program Files\"App");
// Expands to: const char *path = "C:\\Program Files\\\"App\"";Comments within arguments are completely removed and replaced with a single space before the stringification process begins.
The Argument Prescan Rule and Two-Level Macro Expansion
A critical rule of the preprocessor is that macro arguments preceded
by # (or adjacent to ##) are not expanded
prior to stringification. If an argument is itself a macro, passing it
directly to a stringifying macro produces a string of the macro's name,
not its value.
#define STRINGIFY(x) #x
#define BUFFER_SIZE 1024
const char *name = STRINGIFY(BUFFER_SIZE);
// Expands to: const char *name = "BUFFER_SIZE"; (Not "1024")To stringify the expanded value of a macro, an indirect two-stage macro pattern must be used:
#define STRINGIFY(x) #x
#define TO_STRING(x) STRINGIFY(x)
#define BUFFER_SIZE 1024
const char *val = TO_STRING(BUFFER_SIZE);
// Expands to: const char *val = "1024";Because TO_STRING(x) does not use the #
operator directly on x, the preprocessor performs standard
argument prescan and expands BUFFER_SIZE to
1024. It then passes 1024 into
STRINGIFY, yielding the string "1024".
Practical Applications
Stringification is widely used in systems programming for debugging, diagnostics, and code generation.
Variable Logging
By capturing both the expression text and its evaluated value, developers can print variable names dynamically:
#define PRINT_INT(var) printf("%s = %d\n", #var, var)
int counter = 42;
PRINT_INT(counter); // Prints: counter = 42Custom Assertions
Stringification allows custom assertion handlers to display the exact failed condition as source text:
#define MY_ASSERT(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "Assertion failed: %s at line %d\n", #condition, __LINE__); \
} \
} while (0)