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
- Define the Data: Create a macro (or a separate file) that lists items by calling a placeholder macro (usually named
X). - Define X: Locally define what
Xshould expand to (e.g., an Enum value). - Expand: Run the list.
- 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.