Post

X Macros

X Macros

Most people hate macros. They make your code hard to reason about, can easily add subtle bugs, and are ugly. But this macro technique genuinely makes your code much cleaner, safer, and less repetitive. It’s still ugly though.

What do X-macros solve?

Basically code duplication… Consider the enum

1
2
3
4
5
enum class Color {
    Red,
    Green,
    Blue
};

Now if you try std::cout << Color::Red, it will just print 0. So we need a conversion function for this. Something like

1
2
3
4
5
6
7
8
const char* to_string(Color c) {
    switch(c) {
        case Color::Red:   return "Red";
        case Color::Green: return "Green";
        case Color::Blue:  return "Blue";
    }
    return "Unknown";
}

But now let’s say you need to add another color Yellow, but you forget to add it to the to_string function, OR worse, you mess up the order and now your debug logs lie to you.

Solving this with X-Macro

Instead of repeating the list at multiple places we define the following macro

1
2
3
4
5
#define COLOR_LIST \
    X(Red)         \
    X(Green)       \
    X(Blue)        \
    X(Yellow)

What’s this X doing here you may ask… Well that’s the X-Macro! X here can be whatever your heart desires! Let’s see how this changes the enum definition, and the to_string function

1
2
3
4
5
enum class Color {
#define X(name) name,
    COLOR_LIST
#undef X
};

So X(name) is just a temporary macro function here. X(Red) just becomes Red,, so our enum after the preprocessor magic just becomes

This is pretty cool right? Yes, it’s slightly ugly. But it is still far better than code duplication.

1
2
3
4
5
enum class Color {
    Red,
    Green,
    Blue,
};

And why #undef X, well because macros are global, and don’t respect scoping rules, leaving them can cause hard to debug issues. Something like int X = 10 will break if you don’t undef it.

The to_string function, doesn’t get much pretty either…

1
2
3
4
5
6
7
8
const char* to_string(Color c) {
    switch(c) {
#define X(name) case Color::name: return #name;
        COLOR_LIST
#undef X
    }
    return "Unknown";
}

Because of how # in macros works, something like X(Red) becomes case Color::Red: return "Red"; Macros are just so flexible, they can allow you to do pretty much anything.

If you have the privilege of using a C++26 compiler, well, then I have good news for you, you can use reflection instead! No more macro managing.

Thank you!

This post is licensed under CC BY 4.0 by the author.