X Marks the Spot: The X Macro

The X Macro is one of those strange code artifacts that, if one came across it without comments or documentation, it would as unfathomable as it would be ungoogle-able. To steal a snippet from the wikipedia article on the subject:

#define LIST_OF_VARIABLES \
    X(value1) \
    X(value2) \
    X(value3)

#define X(name) int name;
LIST_OF_VARIABLES
#undef X

void print_variables(void)
{
#define X(name) printf(#name " = %d\n", name);
LIST_OF_VARIABLES
#undef X
}

What is happening

The X Macro, that's what! Despite the impenetrable code above, what's been created is the ability to create a list of values once but to define it twice: once as a set of numeric values, and again as a set of printable/scannable strings linked to the numeric values. While a neat trick, why would anyone want such a thing?

Well, how about logging levels? Imagine defining the levels of a log utility (DEBUG, INFO, WARNING, etc.) once but being able to generate both an enum and a container of strings indexible by said enum? One would be able to write such code as:

static log_level_t active_log_level;

...

void write_log_message(log_level_t log_level, const char* msg) // let's assume msg is '\0' terminated
{
    if (log_level < active_log_level) {
        return;
    }
    sprintf(log, "%s: %s\n", log_levels[log_level], msg);
    /*
     * Example log output:
     * "DEBUG: Made you look."
     * "WARNING: Ok you really need to look this time!"
     * "FATAL: lol j\k made u look again lmao X-D"
     */
}

But knowing its worth still doesn't explain its operation. There's two things going on inside the x macro, redefinition and stringification. I'll build on the logging levels idea as an example:

#define LOG_LEVELS \
    LOG_LEVEL(DEBUG) \
    LOG_LEVEL(INFO) \
    LOG_LEVEL(WARNING) \
    LOG_LEVEL(ERROR) \
    LOG_LEVEL(FATAL)

While LOG_LEVELS (plural) is formally defined here, LOG_LEVEL (singular) isn't yet. Seems strange to make a #define full of undefined things, but redefining the inner macro LOG_LEVEL is a big part of the X Macro pattern. We define it specifically to suit our needs for each step. Like below, where we want to make an enum:

#define LOG_LEVEL(level) level,
typedef enum {
    LOG_LEVELS
} log_level_t;
#undef LOG_LEVEL

I've now defined the inner macro LOG_LEVEL but before applying that, let me expand the expand the use of LOG_LEVELS here a bit (line breaks added for clarity):

typedef enum {
    LOG_LEVEL(DEBUG) 
    LOG_LEVEL(INFO)
    LOG_LEVEL(WARNING)
    LOG_LEVEL(ERROR)
    LOG_LEVEL(FATAL)
} log_level_t;

...which, since LOG_LEVEL has been defined to merely take its included string and append a comma, becomes:

typedef enum {
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL,
} log_level_t;

Which just looks like a normal enum, so why the trouble? The real value shows itself the next step, where we make an array of the string form of the names of enums with only one extra #define!

#define LOG_LEVEL(level) #level, // Stringifier macro!
static const char* log_levels[] = {
    LOG_LEVELS
};
#undef LOG_LEVEL

Here's where the X Macro starts to shine. First, let's unroll the above:

static const char* log_levels[] = {
    LOG_LEVEL(DEBUG) 
    LOG_LEVEL(INFO)
    LOG_LEVEL(WARNING)
    LOG_LEVEL(ERROR)
    LOG_LEVEL(FATAL)
};

LOG_LEVEL still appends a comma, but now it also uses the stringifier macro! The single pound sign (or hashtag symbol) # is a built-in macro in most compilers (certainly gcc). From the gcc manual:

"When a macro parameter is used with a leading ‘#’, the preprocessor replaces it with the literal text of the actual argument, converted to a string constant."

Search the gcc manual for "Stringification" for more info. What it does for us here is preprocess our array into the following:

static const char* log_levels[] = {
    "DEBUG", 
    "INFO",
    "WARNING",
    "ERROR",
    "FATAL",
}

...leaving us an array of string forms of our enums. Further, the array lookup links the enum values to their string forms! For example:

printf("%s\n", log_levels[DEBUG]);  // prints "DEBUG"
printf("%s\n", log_levels[ERROR]); // prints "ERROR"

And the X Macro isn't just limited to C, either. It works perfectly fine in C++ as well:

// Imagine this is in an hpp file

#define LOG_LEVELS \
    LOG_LEVEL(DEBUG) \
    LOG_LEVEL(INFO) \
    LOG_LEVEL(WARNING) \
    LOG_LEVEL(ERROR) \
    LOG_LEVEL(FATAL) 

#define LOG_LEVEL(log_level) log_level,
enum log_level_t {
    LOG_LEVELS
};
#undef LOG_LEVEL

#define LOG_LEVEL(log_level) #log_level,
static const std::vector<std::string> log_levels {
    LOG_LEVELS
};
#undef LOG_LEVEL

Because a vector supports the random access operator [], it can be used like an array and still keeps the X macro property where the value of an enum looks up its string form.

And that's just entry level X Macro fun! Stay tuned for the two-argument X Macro, or as I like to call it, the XX Macro.