The X-Y macro [entries|reading|network|archive]
simont

[ userinfo | dreamwidth userinfo ]
[ archive | journal archive ]

Wed 2019-07-03 09:41
The X-Y macro

Here's a fun piece of C preprocessor trickery I thought up earlier this year.

Background

There's a fairly standard existing trick, rediscovered several times and going by several names (‘list macro’, ‘higher-order macro’, or ‘X-macro’ from the traditional name of its parameter), for making a C preprocessor macro encode a list of items in a way that's reusable. You do something like this:

#define LIST_OF_MUSTELIDAE(X) \
X(stoat) X(weasel) X(badger) X(otter)

in which the macro takes a parameter X, which is expected to be another macro. Now you can iterate over this list doing lots of different things, by invoking the macro with different macros as parameters. For example:

#define PREFIXED_ENUM_DECL(val) M_ ## val,
enum Mustelid { LIST_OF_MUSTELIDAE(PREFIXED_ENUM_DECL) };
// expands to M_stoat, M_weasel, M_badger, M_otter,
#define STRINGIFIED_NAME(val) #val ,
const char *const names[] = { LIST_OF_MUSTELIDAE(STRINGIFIED_NAME) };
// expands to "stoat", "weasel", "badger", "otter",

and now you've declared an enumerated type, and an array of textual names, in such a way that there's no risk of them getting out of sync with each other, because both are generated from the same source, namely the original definition of the X-macro.

There are assorted variants on this. If you need a list that has two different kinds of thing interleaved, you can make your list macro take two (or more) macro parameters, and choose which one to invoke for each list element. Or you can make your list macro expect its parameter macro to accept many parameters, so that you can declare a list of things with several properties…

#define ERROR_MESSAGES(X) \
X(ERR_UNRECOGNISED, "unrecognised mustelid name") \
X(ERR_TAIL_COLOUR, "this mustelid's tail isn't that colour") \
// …

… and then each time you invoke it, the parameter macro can use some or all of its arguments, as it chooses.

The X-Y macro

A twist on this that I thought up earlier this year is a thing I'm currently calling an ‘X-Y macro’, for the pretty tenuous reason that instead of taking a single parameter traditionally called X, it takes two which I'm calling X and Y.

The key point is that Y is passed as one of the parameters to X. So your list macro itself might look like this:

#define XYLIST_OF_MUSTELIDAE(X,Y) \
X(Y,stoat) X(Y,weasel) X(Y,badger) X(Y,otter)

Of course, this version can do all the same things the more usual X-macro can do: you just have to make sure all your parameter macros ignore their extra argument. But now you can do a couple more things as well.

Modified list inclusion

Suppose you want to define two lists of things, one of which is a sublist of the other. For example, my program might need a list of mammals, which needs to include everything in the list of Mustelidae I've declared above, plus other things too.

Of course, you can do that with an ordinary X-macro, by making the list macro for the longer list invoke the shorter one, passing it the same X:

#define LIST_OF_MUSTELIDAE(X) \
X(stoat) X(weasel) X(badger) X(otter)
#define LIST_OF_MAMMALIA(X) \
X(cow) X(pig) LIST_OF_MUSTELIDAE(X) X(sheep)

But now suppose you wanted to modify each element of the sublist. For example, suppose I wanted a list of strings, some of which are the stringified versions of the mustelid names, and others are custom. I can't do this:

#define LIST_OF_STRINGS(X) \
X("custom string") X("hello") LIST_OF_MUSTELIDAE(X) X("blither")

because the sub-list-macro will expand to unstringified names, and when I try to use the result to populate an array, I'll get a type error.

But if the subsidiary list is defined by an X-Y list macro, you can do it:

#define STRINGIFY_AND_CALL(X, value) X(#value)
#define LIST_OF_STRINGS(X) \
X("custom string") X("hello") \
XYLIST_OF_MUSTELIDAE(STRINGIFY_AND_CALL, X) \
X("blither")

because the X-Y list will expand to a sequence of things along the lines of STRINGIFY_AND_CALL(X,weasel), which in turn expands to X("weasel"), which matches the form of the list entries that were expressed directly.

Parametric expression macros

Another thing you can do with an X-Y macro is to embed the whole list of items in a macro that expands to an expression.

For example, suppose I had a list macro defining a set of identifiers each with a specific value, and I wanted to test some input value against all of them in some way.

#define MAGIC_CONSTANTS(X) X(fizz, 3) X(buzz, 5)

If I only need to do the test at run time, then of course I can just use a standard X-macro, by defining a function:

const char *optionally_replace(int input, const char *default_string)
{
#define TEST_AND_RETURN(name, val) \
if (input % val == 0) return #name;
MAGIC_CONSTANTS(TEST_AND_RETURN)
// expands to: if (input % 3 == 0) return "fizz";
// if (input % 5 == 0) return "buzz";
return default_string;
}

But if I want to generate an array at compile time – for example, because the results of the substitution have to go into a constant array declaration – then I need to do this in the form of a macro that takes the input value to test as a parameter. For that, I need my parameter macro to expand to a conditional expression of the form (input % value == 0), but where does it get input from?

From the extra parameter Y, of course:

#define XY_MAGIC_CONSTANTS(X,Y) X(Y, fizz, 3) X(Y, buzz, 5)
#define TEST_CLAUSE(input, name, val) input % val == 0 ? #name :
#define REPLACE(n) ( XY_MAGIC_CONSTANTS(TEST_CLAUSE, n) #n )
// expands to: (TEST_CLAUSE(n,"fizz",3) TEST_CLAUSE(n,"buzz",5) #n)
// -> (n % 3 == 0 ? "fizz" : n % 5 == 0 ? "buzz" : #n)
const char *const names[] = {
REPLACE(1), REPLACE(2), REPLACE(3),
REPLACE(4), REPLACE(5), REPLACE(6)
// -> "1", "2", "fizz", "4", "buzz", "fizz"
};

Afterthoughts

Of course, this is very similar to a standard pattern used for callback functions in ordinary runtime C: it's common to define your callback function as taking a void * context parameter, and then whatever is going to call the callback, you pass it an argument pair (f, ctx) consisting of the function pointer and the context you want it to use, and then it calls f(ctx, stuff) repeatedly. But until this year I'd never seen a need to do the same thing at preprocessor time!

I don't doubt that in some situation with yet another level of indirection you'd need a list macro taking three parameters and evaluating X(Y,Z,foo), X(Y,Z,bar), and so on. Probably that would come up if you needed a second layer of modified list inclusion, or some such. But I haven't encountered it yet.

LinkReply
navigation
[ go | Previous Entry | Next Entry ]
[ add | to Memories ]