X-Macros in C
What are X-Macros?
C has one neat feature that can be (ab)used to work around many of its shortcomings: The C Preprocessor, which is basically a way to define instructions for copy-and-pasting bits of text together.
X-Macros use this feature to let you define a list, and then generate several bits of code based on that very same list.
You can do this because the preprocessor lets you remove macros using the #undef
preprocessor command so you can define them a second time with a different expansion. Let’s do an example:
#define COLORS X(Red) \
X(Green) \
X(Blue)
// Define an enum:
#define X(name) COLOR_ ##name,
enum Color {
COLORS
COLOR_COUNT
};
#undef X
// Define a string table:
#define X(name) #name,
const char * gColorNames[COLOR_COUNT + 1] = {
COLORS
""
};
#undef X
After the preprocessor has run, that code will look like:
enum Color {
COLOR_Red, COLOR_Green, COLOR_Blue,
COLOR_COUNT
};
const char * gColorNames[COLOR_COUNT + 1] = {
"Red", "Green", "Blue",
""
};
Allowing you to print an enum Color
value like:
enum Color myColor = Green;
printf("myColor = %s\n", gColorNames[myColor]);
And get the printout:
myColor = Green
Since the initial examples of this technique named the macro to be replaced X()
(like we did it above), the technique was named that as well, and it has stuck as a convention, although you could of course pick much more expressive and self-documenting names if you wanted.
Why would I use X-Macros?
X-Macros are very useful when you are dealing with static lists of things that need to be
kept in sync. Be it two actual lists like above, or a list and a switch statement that needs to cover all its cases in a way that follows a simple pattern. The token-pasting operator ##
and the stringify operator #
are very handy in many such cases, to let you take the same data and use it as different data types.
Included X-Macros
One downside of classic X-Macros is that, given you can’t have line breaks in a macro, all your constants end up on the same line. The definition of COLORS
above merely escapes the line breaks, which means they’re not part of the macro output, they just break the definition up over multiple lines to make it more readable. Worse, most IDEs will jump to the COLORS
line in the enum
above, not to the actual definition of the COLOR_Red
entry, when you use their code navigation feature. That makes it harder to find documentation on the individual enum cases you might have written next to the individual X-macros.
One workaround to this is to not define a macro for the COLORS
list, and instead write it into a separate file:
Colors_X.h
X(Red)
X(Green)
X(Blue)
and instead (ab)use #include
to paste that header in twice:
// Define an enum:
#define X(name) COLOR_ ##name,
enum Color {
#include "Colors_X.h"
COLOR_COUNT
};
#undef X
// Define a string table:
#define X(name) #name,
const char * gColorNames[COLOR_COUNT + 1] = {
#include "Colors_X.h"
""
};
#undef X
Each X-Macro is now really on its own line, resulting in the preprocessed code:
enum Color {
COLOR_Red,
COLOR_Green,
COLOR_Blue,
COLOR_COUNT
};
const char * gColorNames[COLOR_COUNT + 1] = {
"Red",
"Green",
"Blue",
""
};
And moreover, the compiler keeps track of line numbers in included files, and when you trigger code-navigation on a COLOR_Red
identifier in your source code, it will now jump to the X(Red)
line in Colors_X.h
, as desired.
But of course the enum and strings array definitions look even less readable now, and you need a separate file for each and every enum.
Should I use X-Macros in my code?
In general, I would avoid using X-Macros. That I got an entire blog post out of the technique should show you that they are not obvious, and the caveats I gave above should show you that X-Macros have their own issues. That said, if you have a C codebase that consists of numerous static look-up tables that need to be kept in sync, X-Macros are likely the least horrible solution. If having out-of-sync tables can lead to hard-to-diagnose bugs, a little education to every new hire about X-Macros via a comment is a small price to pay. So sure, use them there if you can’t use statically declared structs instead.
After all, if you’re already forced to use C instead of a higher-level programming language, you probably have many constraints on you already, and X-Macros might actually lead to more readable and more maintainable code.