If we told you that a C preprocessor hack dated back to 1968, you’d be within your rights to remind us that C didn’t exist in 1968. However, assemblers with preprocessors did, and where there is a preprocessor, there is an opportunity to do clever things. One of those things is the so-called X macro, which saw a lot of use in DEC System 10 code but probably dates back even earlier. You can still use it today if you like, even though there are, of course, other arguably better ways to get the same result. However, the X macro can be very efficient, and you may well run into it in some code, too.

Background

Preprocessing used to be a staple of programming. The idea is that code is manipulated purely at the text level before it is compiled. These days, languages with a preprocessor usually handle it as part of the compiler, but you can also use an external preprocessor like m4 for more sophisticated uses.

Modern languages tend to provide other ways to accomplish many of the tasks handled by the preprocessor. For example, if you have a constant you want to set at compile time, you could say:

int X = 32;
y = X;

But then you’ve created a real variable along with the overhead that might entail. A smart compiler might optimize it away for you, but you can be sure by writing:

#define X=32
y = X;

A modern compiler would prefer you to write:

const int X=32;
y = X;

But there are still some common uses for macros, like including header files. You can also make more sophisticated macros with arguments so you don’t incur a function call penalty, although modern usage would be to mark those functions as inline.

The Problem

Which brings us to the X macro. With all great hacks, there is first a problem to solve. Imagine you have a bunch of electronic parts you want to deal with in your code. You don’t want a database, and you don’t want to carry a bunch of strings around, so you define an enumerated type:

enum parts { part_LM7805, part_NE555 }; // will add more later

Of course, you will eventually want to print them, so you do need to store the names somewhere, right?

const char *partnames = { "LM7805", "NE555" }; // will add more later

This is all fine until you add a new part like, say, a 2N2222. You must remember to update both the enumerated type and the string or havoc will ensue. This seems easy until you realize that you might define the enumerated type in a header file but only define the string array in a source file. It is easy to get them out of sync.

The Hack

The idea is to define a macro that handles all the definitions of parts in one place:

#define PARTS \
X(part_LM7805,"LM7805") \
X(part_NE555,"NE555")

Now when you declare the enum and the string array (which may not be in the same file, remember):

#define X(a,b) a,
enum parts { PARTS };
#undef X #define X(a,b) b,
const char *partnames= { PARTS };
#undef X

If you carefully read the code, you can see how it works. The PARTS macro defines a list of items using the X macro. Before using the list, you define X to “select’ one of the pieces. The first #define makes X() return its first argument, and the second #define, the second. Because these preprocessor macros run before the code is interpreted, this causes the preprocessor to write the same code as in the original example. The advantage is that the ID and the name are joined together in the text which makes it harder to forget to add or update one when making changes.

Even Better

Using modern C preprocessor syntax, we can do even better by using token pasting and the stringize operator.

Here’s a quick tutorial if you haven’t encountered these oddball preprocessor operators. The stringize operator # converts whatever you put after it into a quoted string. The token pasting operator ## joins two tokens into one token. So:

#define print(str) printf("%s\n", #str);
#define declare(type, prefix, var) type prefix##var; declare(int,global_,v);
print(Hello!);

Not that either of these are a good idea, mind you. But you can see that the declare macro will define an integer called global_v and the print macro will print the token that follows it as a string.

Consider this:

 #include <stdio.h>
#define PARTS \ X( LM7805, 0.20 ) \ X( NE555, 0.09 ) \ X( 2N2222, 0.03 ) // create enum
#define X(a, b) part_##a,
enum parts { PARTS };
#undef X //create string table
#define X(a, b) #a,
const char *partnames[]={ PARTS };
#undef X // create price table
#define X(a, b) b,
float partprice[]= { PARTS };
#undef X int main()
{ enum parts p=part_NE555; printf("%s costs %0.2f\n", partnames[p], partprice[p]); printf("%s costs %0.2f\n", partnames[part_2N2222], partprice[part_2N2222]); return 0;
}

Here, we define a table of parts and prices. (Made up prices, to be sure.) The enumerated type uses part_##a to create things like part_NE555. The string table uses #a to get a string “NE555” into the source code. Finally, the price table uses b.

Simple, yet effective. Sure, you could use a structure or an object to help. There are also plenty of other ways you could deal with this in the preprocessor. For example, you could define everything in one file and use #if to select what parts of it are included in different parts of the code. Regardless, the X macro is an elegant hack and it does solve the problem and has been since at least 1968.

The preprocessor can do some pretty amazing things. For example, we’ve built a cross assembler using it. We’ve even seen people do logic gate simulations in the preprocessor.

Source: https://hackaday.com/2023/03/16/the-x-macro-a-historic-preprocessor-hack/