X-Macros in C/C++
Okay, okay. Macros are obscure and Evil, but they can be really useful when you want to generate code easily.
X-Macros have been in use for a long while now, but don’t seem to be well known these days. So let’s see how they can help us.
The scenario
Suppose you’ve defined an enumeration with some colors you support in your application, like so:
You use them throughout your code but then you need to have their string representation, maybe because you have to output them in a debug view. So you decide to create a separate sibling array of strings that match the defined values, with a function that retrieves that representation:
You are happy with your solution and call it a day.
But then, the inevitable happens.
Evolution
Your solution is so solid and works so nicely that you start adding colors all over the place. Let’s add one for now:
It forces you to add a new entry to your array of string representations or you’ll have issues when you call colorToString(Color::YELLOW)
.
Yikes, you must remember to add code in two places!
New requisite: non-sequential IDs
You weren’t done drying your sweat when somebody decides the enumeration won’t start at 0 and keep counting from that because that’s not semantic enough for your domain. So now have:
Oh noes! Your clever sibling array of strings can’t be used anymore!
The naive solution
Okay, okay, don’t panic. Let’s just create a switch
block and forget about the array, huh?
Phew, it’s solved and you can clearly see what’s happening.
But then you add yet another color to your enum. You better remember to add a new case
block. Jeez, this is smelling.
New requisite: string to enum
You’ve got used to the smell and you think Know what? It would be great if we could reference these colors from a data file by using their names. Your fingers still sweating, you create this function:
Note that we’ve added a new INVALID
value in the process. Remember to add that one wherever necessary!
Right, so now you add a new color, say Color::MAGENTA
. You have to:
- Add the value to the enumeration.
- Remember to add a
case
in thecolorToString
function. - Remember to add an
if
in thestringToColor
function.
Oh man, this is so error prone.
X-Macros to the rescue
Let’s start again, but this time we’ll define a Macro with all the colors:
Colors
is a Macro that generates nothing by itself: it’s just a list of invocations to another Macro X
with some data. Let’s use it to create the enumeration we want:
And we’ve got it.
Er… what?
Within the enumeration we’ve defined Macro X
, which receives an argument (the color from the list) and translates it to ID,
. After that, we remove this definition of X
so no other code after this one knows about it and has unexpected results. Let’s expand the code similarly to what the preprocessor would do:
Okay, cool, but what about the string representation? Let’s define the function:
We’ve defined a new version of X
which, this time, expands to case Color::ID: return #ID;
. Again, if we expand the code we have:
Yeah, it’s hard to read, but we don’t mind as it’s generated under the hood for us and the compiler doesn’t care about spacing.
Great, you’re filled with happiness and smile at your solution. Then, the inevitable happens.
Evolution
You want to add a new color and your legs shake in fear. So you modify the Colors
macro to include it, like so:
Now you invoke colorToString(Color::YELLOW)
and… it works! Automatically, both the X
macro within the enum
and the one within colorToString
included new code for your new color! We didn’t have to remember adding it, so that’s good.
New requisite: non-sequential IDs
We’re still celebrating our intelligence when somebody decides the enumeration won’t start at 0 and keep counting from that because that’s not semantic enough for your domain. To do that, you modify Colors
this way:
And now, we have to modify the definitions of X
. So we have:
Note that we aren’t using the second argument of X
in colorToString
because we aren’t interested in it.
A call to static_cast<int>(Color::GREEN)
would yield the expected 7
result.
Alright, we’re not sweating yet, that’s good.
New requisite: string to enum
We’re back to loading data from a file that uses the string representations of our enumeration. This would be the function that does the trick:
Yes, it’s still the same code but you don’t have to mantain it manually whenever a new element is added.
If you’ve been reading carefully, there’s a new element in the enumeration called INVALID
. This one is an internal one, not an user-defined one. So, we’d have to add it to the enumeration itself, not as an item in Colors
:
The litmus test
So far we’ve defined a Macro called Colors
that does nothing by itself, an enumeration Color
that defines its own version of X
, a function colorToString
that also defines its own version of X
and a function stringToColor
that defines yet another version of X
. That looks complex, right? Let’s put it to a test by adding a new color!
We add a new entry in the Colors
Macro:
And that’s it. Everything else just works.
We didn’t have to remember to add anything else than the new color. Very similar to adding a new entry to an actual enumeration.
Now we’re smiling and very happy :)
The downsides
Like everything else, with great power comes great responsibility. By using X-Macros:
- You lose the ability to debug any expansion of
X
: be careful with having complex definitions that can fail in many places. Test those definitions standalone before wrapping them into a X-Macro. - You’re giving your team a hard time: chances are they aren’t used to X-Macros, so they could feel lost until they understand what’s going on.
- You shouldn’t forget you haven’t written the whole code, but it’s there! If you perform costly computations within the expansions of
X
, those won’t magically disappear! - You can get dragged into the hype train and start using this feature all over the place. Remember: this isn’t a silver bullet.
Bonus: interesting usage
Alright, so I’ve convinced you that X-Macros are useful but what can you do with them apart from having the string representation of an enumeration? Let’s talk about one.
Bulk member variables definition
Have you ever forgot to initialize a member variable in the constructor? Maybe you wanted to have automatic getters and setters for every member you defined? This could do the trick for you:
So now you could do stuff like:
One of the downsides is having to define all of your members within the X-Macro, and that’s where macros can make your code uglier.
Thank you for reading!