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:

  1. 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.
  2. 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 = 42

Custom 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)