Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

What are X macros and when to use them?

+4
−0

Occasionally I run into some strange pre-processor code with a list like this:

#define LIST \
  X(1)       \
  X(2)       \
  X(3)       \

And then code followed by other obscure macros and macro calls like:

typedef enum
{
  #define X(n) ITEM_##n, 
    LIST
  #undef X
} list_t;

These are apparently referred to as X macros. The Wikipedia page is bleak when it comes to describing what these are acutally good for.

What are X-macros good for and when should they be used? Which style is preferred? What are the advantages and disadvantages?

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

1 answer

+2
−0

Purpose and use

X macros is a design pattern used for the purpose of centralizing data & code maintenance to a single point in the program. Instead of maintaining code based on some data set in multiple places, we can gather the data at one single place, in a macro. And it's all done at compile-time, so there's no run-time overhead.

The idea is that all data, for which we need to do a certain thing with repeatedly, is gathered in a list, the "X macro list". Each item is placed inside a parenthesized, comma-separated expression with all data that belongs together:

#define ANIMAL_LIST                \
  /*animal   legs  sound */        \
  X(cat,     4,    meow)           \
  X(dog,     4,    woof)           \
  X(rooster, 2,    cockadoodledoo) \

Each item inside the comma-separated list should preferrably be a valid pre-processor token. We could write each such item as multiple pre-processor tokens too, but that will restrict the flexibility somewhat.

Each X in this list will boil down to a function-like macro call. And so when we use ANIMAL_LIST there will be 3 macro calls immediately after each other. Since ANIMAL_LIST is one big macro we have to use \ to separate the lines into something readable for humans.

In the first example from the question and from Wikipedia, they use the style of #define X(...) something, then call the macro X over and over. Then finally #undef X to make the macro name X usable again. Example:

#define X(animal, legs, sound) \
  printf("A %s has %d legs and says '%s'.\n", #animal, legs, #sound);

  ANIMAL_LIST  /* note the lack of semicolon here */
#undef X

Note that since this is still macros, we may use the # "stringification operator" to convert something to a string whenever we want. Or use ## to concatenate symbols. The above example will result in a pre-processor output like:

printf("A %s has %d legs and says '%s'.\n", "cat", 4, "meow"); 
printf("A %s has %d legs and says '%s'.\n", "dog", 4, "woof"); 
printf("A %s has %d legs and says '%s'.\n", "rooster", 2, "cockadoodledoo");

So we may use this for iterating across data, for generating enums, even for generating code.


Preferred style

I strongly recommend a different style than the #define X / #undef X one though. As noted, the #define X ... #undef X adds a bit of extra clutter. But it also looks kind of alien to type out a macro name ANIMAL_LIST without any parenthesis or semicolon.

There is a different style that removes that clutter while at the same time making the X macros even more flexible. In the preferred style we pass the macro that should be called for each data item as a parameter to the list macro itself:

 #define ANIMAL_LIST(X)

Now that's a bit confusing but more powerful than the previous. On the caller side we wouldn't use the generic identifier X any longer, but instead define a named macro:

#define ANIMAL_PRINT(animal, legs, sound) ...

And then call the list as

ANIMAL_LIST(ANIMAL_PRINT)

This gives less clutter and more self-documenting names. But now we may not just call a list of data with different macros - we may also use the macro for different sets of identical data.

MAMMAL_LIST(ANIMAL_PRINT)
BIRD_LIST(ANIMAL_PRINT)

An example where two data lists are used for printing of lots of diverse data:

#include <stdio.h>
#include <stdint.h>

#define DATA_LIST1(X)      \
  X(int,    123,      %d)  \
  X(double, 3.1415,   %f)  \
  X(char*,  "hello",  %s)  \

#define DATA_LIST2(X)      \
  X(char,   'A',      %c)  \
  X(size_t, SIZE_MAX, %zu) \

#define DATA_PRINT(type, val, fmt) printf("%s: " #fmt "\n", #type, val);

int main() 
{
  DATA_LIST1(DATA_PRINT)
  DATA_LIST2(DATA_PRINT)
}

Advanced example

Here is another advanced example mostly just to demonstrate how ridiculously flexible these can get. (If you don't understand the following code then don't worry! It is using a lot of different C tricks all at once.)

Lets define one X macro list for all types we want to support and their respective printf format specifiers:

#define SUPPORTED_TYPES(X) \
  X(int,    %d)            \
  X(double, %f)            \
  X(char,   %c)            \

Then use this to generate code for 3 different functions, print_int, print_double and so on:

#define GENERATE_FUNCTIONS(type, fmt) \
  void print_##type (type param)      \
  {                                   \
    printf(#fmt "\n", param);         \
  }
SUPPORTED_TYPES(GENERATE_FUNCTIONS)

Here the token concatenation operator ## was used, which wouldn't be possible if we had items in the list consisting of more than one pre-processor token, for example char*. Nor would it be possible if the list contained items which can't exist in a C identifier, print_char* would be invalid syntax.

Next up suppose we have a list of data:

#define DATA_LIST(X)       \
  X(123)                   \
  X(3.1415)                \
  X((char)'A')             \

And we wish to print this with a generic function interface print(), no matter which data we put in, to call the various type-specific functions defined earlier. Such a function call could be made type safe with for example:

#define print(val) _Generic(val, int: print_int) (val)

And then just add double: print_double etc for each supported type. But typing that manually is too easy :) And not generic enough - why not use our supported types X macro for the generic association list itself:

#define GENERIC_LIST(type, fmt) ,type: print_##type
#define print(val) _Generic((val) SUPPORTED_TYPES(GENERIC_LIST)) (val)

Note that the fmt parameter of the X macro was completely ignored, which is something we can always chose to do. Also in the specific case of _Generic, it doesn't support trailing comma, hence the peculiar syntax with , first.

And finally, to completely soak ourselves in X macros, lets call that generic print macro for each item in our data list:

#include <stdio.h>

#define SUPPORTED_TYPES(X) \
  X(int,    %d)            \
  X(double, %f)            \
  X(char,   %c)            \

#define GENERATE_FUNCTIONS(type, fmt) \
  void print_##type (type param)      \
  {                                   \
    printf(#fmt "\n", param);         \
  }
SUPPORTED_TYPES(GENERATE_FUNCTIONS)

#define GENERIC_LIST(type, fmt) ,type: print_##type
#define print(val) _Generic((val) SUPPORTED_TYPES(GENERIC_LIST)) (val)

#define DATA_LIST(X)       \
  X(123)                   \
  X(3.1415)                \
  X((char)'A')             \

int main() 
{
  #define PRINT_LIST(val) print(val);  
  DATA_LIST(PRINT_LIST)
}

https://godbolt.org/z/cq7YarM66 (note how it just boils down to 3 printf calls with optimizations on)

Preprocessed code (re-formatted here for readability):

void print_int (int param) 
{ 
  printf("%d" "\n", param); 
} 

void print_double (double param) 
{ 
  printf("%f" "\n", param); 
} 

void print_char (char param) 
{ 
  printf("%c" "\n", param); 
}

int main()
{
  _Generic((123),
           int:    print_int,
           double: print_double,
           char:   print_char) (123); 

  _Generic((3.1415),
           int:    print_int,
           double: print_double,
           char:   print_char) (3.1415); 

  _Generic(((char)'A'),
           int:    print_int,
           double: print_double,
           char:   print_char) ((char)'A');
}

Advantages of X macros:

  • Very generic and powerful. Data can be turned into identifiers and/or strings as needed.
  • Reduces code repetition drastically.
  • Ideal for reducing code maintenance to a single place in the code base.
  • Ideal for maintaining some old code base where someone already made a fine mess and we need to bring order to it.
  • A de facto industry standard way of writing macros, instead of cooking up some home-brewed, project-specific macro solutions (very bad practice).

Disadvantages of X macros:

  • Hard to read, especially for those not used to seeing them.
  • Hard to trouble-shoot while writing them.
  • Although easy to maintain on the code side, they are hard to debug in a debugger - similar to attempting to debugging for example inlined functions.

I always recommend to use X macros as a last resort, rather than the first thing that comes to mind. For example maintaining a bunch of array tables with related data and a common index used to tie that data together is very readable and reasonably fast as well.

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »