X-Macros

Published:

Macros

Macros are just like any other tool - a hammer used in a murder is not evil because it’s a hammer. It is evil in the way the person uses it in that way. If you want to hammer in nails, a hammer is a perfect tool. There are a few aspects to macros that make them “bad”:

  1. You can not debug macros.
  2. Macro expansion can lead to strange side effects.
  3. Macros have no namespace
  4. Macros may affect things you don’t realize.

Let me explain what I’m refering!

1. You can not debug macros:

When you have a macro that translates to a number or a string, the source code will have the macro name, and many debuggers, you can’t “see” what the macro translates to. So you don’t actually know what is going on. The conclusion is you can not debug macros or you will use enum or const T.

2. Macro expansion can lead to strange side effects.

The famous one and the well known example is.

#define min(X, Y) ((X) < (Y) ? (X) : (Y))

What is wrong with this macro ? Let me show you

#include <stdio.h>
#define min(a, b) ((a) < (b)) ? (a) : (b)
int main() 
{
	int a = 1, b = 2;
	printf ("%d\n", min (a, b));
	printf ("a=%d, b=%d\n\n", a, b);
	printf ("%d\n", min (a++, b++));
	printf ("a=%d, b=%d\n\n", a, b);
}

The code will output:

$ gcc test.c && ./a.out
1
a=1, b=2
2
a=3, b=3

It’s a classic trick. Double evaluation!!

3. Macros have no namespace

If you have a macro that clashes with a name used elsewhere, you get macro replacements where you didn’t want it, and this usually leads to strange error messages. If we have a macro:

#define begin() x = 0

and we have some code in C++ that uses begin:

std::vector<int> v;
... stuff is loaded into v ...

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
	std::cout << ' ' << *it;

Now assuming you have completely forgotten - or didn’t even know about - the begin macro that lives in some header file that someone else wrote? What error message do you think you get, and where do you look for an error? And even more fun if you included that macro before the include - you’d be drowning in strange errors that makes absolutely no sense when you look at the code itself.

4. Macros may affect things you don’t realize.

#define begin() x = 0
#define end() x = 17

... a few thousand lines of stuff here ...

void dostuff()
{
	int x = 7;
	begin();
	
	... more code using x ...
	
	printf("x=%d\n", x);
	end();
}

Without looking at the macro, you would think that begin is a function, which shouldn’t affect “x”, but it is big fail!!!

Macros are obscure and evil, but they can be really useful when you want to generate code easily

To use a corny example, let’s have a header file color.h, and in that there’s an enum for the colors:

enum Color { eRed, eBlue, eGreen };

Manny times not enough use them throughout your code, you need to have their string representation. E.g. you have to output them in a debug view. So the way to print colors, there’s a corresponding array of strings will be something like this:

static char *ColorStrings[] = {"red", "blue", "green"};

You are happy with your solution and call it a day. But then, the inevitable happens. You have to add the new color.

enum Color { eRed, eYellow, eBlue, eGreen };

And yes, we forget to update the ColorStrings[] array, and not only does printing out Cyellow come out as “blue” even worse we have an array overflow printing out the string for Cgreen. You must remember to add code in two places!

Let’s we do it in other ways:

Define the enum with the new requisite: non-sequential IDs

enum class Color
{
	eRed =   1,
	eBlue =  2,
	eGreen = 4,
	eCount
};

And conversation functions, getColorFromString and getStringFromColor.

Color getColorFromString(const char* aStr)
{
	if (!strcmp(str, "Red")) return eRed;
	if (!strcmp(str, "Blue")) return eBlue;
	if (!strcmp(str, "Green")) return eGreen;
	return eAnimal_Count;
}
const char* getStringFromColor(Color aColor)
{
	switch (aColor)
	{
		case eRed: return "Red";
		case eBlue: return "Blue";
		case eGreen: return "Green";
		default: return "None";
	}
}

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 the colorToString function. • Remember to add an if in the stringToColor function. That was horrendous enough. There must be a better solution.

The X-Macro

Let’s start again, but this time we’ll define a Macro with all the colors:

#define Colors \
	X( RED )   \
	X( BLUE )  \
	X( GREEN ) \

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:

enum class Color
{
	#define X(ID) ID,
		Colors
	#undef X
};

And we’ve got it.

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:

enum class Color
{
	eRED,
	eBLUE,
	eGREEN,
};

And the string representaion’s function is:

const char *colorToString(Color color)
{
	switch (color)
	{
		#define X(ID) case Color::ID: return #ID;
			Colors
		#undef X
	};
	return nullptr;
}

This way adding a new color becomes trivial and the enum and array magically get updated automacally. The more experienced programmers will immediately see this can be made more sophisticated.

What is the next level?

Define the enum enumeration by Macro X with the new requisite: non-sequential IDs.

#define Colors       \
	X( RED,      1 ) \
	X( BLUE,     2 ) \
	X( GREEN,    4 ) \
	X( YELLOW,  16 ) \
	X( MAGENTA, 32 ) \

And now, we have to modify the definitions of X. So we have:

enum class Color
{
	#define X(ID, VALUE) ID = VALUE,
		Colors
		INVALID
	#undef X
};
	
const char *colorToString(Color color)
{
	switch (color)
	{
		#define X(ID, VALUE) case Color::ID: return #ID;
			Colors
		#undef X
	};
	return nullptr;
}

Note that we aren’t using the second argument of X in colorToStringbecause we aren’t interested in it. A call to static_cast(Color::GREEN) would yield the expected 7 result.

Color stringToColor(const char *colorName)
{
	#define X(ID, VALUE) if(strcmp(colorName, #ID) == 0) return Color::ID;
		Colors
	#undef X
	return Color::INVALID;
}
printf("%i\n", static_cast<int>(Color::MAGENTA)); 			// 32
printf("%s\n", colorToString(Color::MAGENTA)); 				// MAGENTA
printf("%i\n", static_cast<int>(stringToColor("MAGENTA"))); // 32

The X Macro technique works with any language with a half-decent text macro preprocessor, and C’s is certainly up to the task. Use it and pass it along, as my friends were kind enough to pass it along to me.

Thank you for reading!