Skip to content

X Macros

1. Concept

X-Macros are a design pattern used to maintain a list of data items in a single location (the "source of truth") and generate multiple code constructs (Enums, String Arrays, Switch Cases) from that list. This adheres to the DRY (Don't Repeat Yourself) principle.

2. Mechanism

  1. Define the Data: Create a macro (or a separate file) that lists items by calling a placeholder macro (usually named X).
  2. Define X: Locally define what X should expand to (e.g., an Enum value).
  3. Expand: Run the list.
  4. Redefine X: Undefine X, define it differently (e.g., as a String), and run the list again.

3. Comprehensive Example: State Machine Management

Imagine we need to manage a system's state. We need: 1. An enum for the states. 2. An array of strings to print the state names for debugging. 3. A switch statement to handle logic.

Instead of writing this in three places (which causes bugs if you forget to update one), we use X-Macros.

Step 1: Define the "Table"

// LIST_OF_STATES(X)
// The 'X' will be replaced by our temporary macro definition later.
// Arguments: (EnumName, StringDescription, ID)
#define STATE_TABLE(X) \
    X(STATE_INIT,   "Initializing", 10) \
    X(STATE_IDLE,   "System Idle",  20) \
    X(STATE_ACTIVE, "Processing",   30) \
    X(STATE_ERROR,  "Fatal Error",  99)

Step 2: Generate the Enum

// Define X to extract just the Enum Name and ID
#define X(name, str, id) name = id,

typedef enum {
    STATE_TABLE(X) 
    // Expands to:
    // STATE_INIT = 10,
    // STATE_IDLE = 20,
    // ...
} SystemState;

#undef X // Always clean up!

Step 3: Generate the String Array

// Define X to extract just the String
#define X(name, str, id) str,

const char* StateNames[] = {
    STATE_TABLE(X)
    // Expands to:
    // "Initializing",
    // "System Idle",
    // ...
};

#undef X

Step 4: Generate a Helper Function (Switch Case)

// Define X to generate a case statement
#define X(name, str, id) case name: printf("Handling %s...\n", #name); break;

void handle_state(SystemState s) {
    switch(s) {
        STATE_TABLE(X)
        // Expands to:
        // case STATE_INIT: printf(...); break;
        // case STATE_IDLE: printf(...); break;
        // ...
        default: printf("Unknown State\n"); break;
    }
}

#undef X

4. Why use X-Macros?

  • Maintenance: Adding a new state requires changing code in only one place (STATE_TABLE).
  • Synchronization: It is impossible for the Enum and the String Array to get out of sync.
  • Readability: While the macro definition looks complex, the usage ensures the data relationship is clear.