X-Macros

When looking for X-Macros on Google, the only reference I came across was this one (ddj.com), which is also the place where I happened across X-Macros by accident (in the printed version of the magazine). I have since put them to great use in production code. What are X-Macros? They are basically a clever way to avoid code duplication, using the C/C++ preprocessor. They come in very handy whenever you would normally need to hunt down and hand-edit multiple places in your code after expanding or modifying some lower level layer which this code depends upon. Let's look at some examples to find out what X-Macros might be able to do for you.

The "Enum Example"

Let's say you have an enum, and you would like to associate a string with each element, to provide a readable name. A common way to do this is as follows:

C++:
  1. #include <iostream>
  2.  
  3.  enum IDs
  4.  {
  5.      ID0 = 0,
  6.      ID1,
  7.      ID2,   
  8.      NUM_IDS
  9. };
  10.  
  11. const char* idStrings[] =
  12. {
  13.     "ID 0",
  14.     "ID 1",
  15.     "ID 2",
  16.     NULL
  17. };
  18.  
  19.  
  20. int main()
  21. {
  22.     std::cout <<"ID0 -> " <<idStrings[ID0 ] <<std::endl;
  23.     std::cout <<"ID1 -> " <<idStrings[ID1 ] <<std::endl;
  24.     std::cout <<"ID2 -> " <<idStrings[ID2 ] <<std::endl;
  25.  
  26.    return 0;
  27. }


The output of this program is:

ID0 -> ID 0
ID1 -> ID 1
ID2 -> ID 2

Now, whenever you add an element to IDs you will have to remember to add an element to idStrings. The compiler wont tell you about your omission, and things could get nasty only very late at runtime. Even more so, if you have code that operates on all members of the enum, you have to add to this code as well, as you would have to in the above example, where we output all the string names on standard output.

X-Macros relieve you of all that. Let's rewrite our code like this:

C++:
  1. #include <iostream>
  2.  
  3. // (1) Define code generating macro
  4. #define GENERATE_IDS    \
  5.     X(ID0, "ID 0")      \
  6.     X(ID1, "ID 1")      \
  7.     X(ID2, "ID 2")
  8.    
  9. // (2) Define X-Macro for generating enum members
  10. #define X(id, idString) id,
  11.     enum IDs
  12.     {   
  13.         // To be on the safe side,
  14.         // force ID's to start at 0
  15.         ID_LOWERBOUND = -1,
  16.         GENERATE_IDS
  17.         NUM_IDS
  18.     };
  19. #undef X
  20.  
  21. // (3) Define X-Macro for generating string names
  22. #define X(id, idString) idString,
  23.     const char* iDStrings[] =
  24.     {   
  25.         GENERATE_IDS
  26.         NULL
  27.     };
  28. #undef X
  29.  
  30. int main()
  31. {
  32.     std::cout <<"ID0 -> " <<idStrings[ID0 ] <<std::endl;
  33.     std::cout <<"ID1 -> " <<idStrings[ID1 ] <<std::endl;
  34.     std::cout <<"ID2 -> " <<idStrings[ID2 ] <<std::endl;
  35.  
  36.    return 0;
  37. }


Let's examine what is happening here. At (1) we define a macro which expands to a set of "X-Macros". In our case, the X-Macro has 2 Parameters. At this point in the code, they are not expanded. Note that this uses the fact that the preprocessor will not expand macros embedded in other macros, before the "parent" macro is expanded. Therefore it does not matter that, at this point, X is not defined to anything. At (2) the whole magic happens. The X-Macro is defined. It will expand to its first parameter (ID0, ID1, ID2) and a colon.
Now, inside the enum definition, we expand GENERATE_IDS. This will now also expand the current definition of our X-Macro, and, voila, an enum is generated. At (3) we do the same thing, only this time, we expand the X-Macro to something else, namely, it's second parameter and a colon, resulting in a list of strings generated by GENERATE_IDS, fitting nicely into our array definition. Note that the definition of X had to be undefined before we could define it again.
Now, whenever we want to add an enum, all we have to do is add a line to the definition of GENERATE_IDS, with the enum value, and the enum string name. There is no need to edit multiple places in the code any longer. The output of the new program will be exactly the same as the first one.

Beyond Enums

While the above example demonstrates a useful technique to avoid code duplication in a particular implementation, X-Macros can be put to use in many different scenarios. I would like to give one more example in this post. Let's say you want to have a class which has many different data members, and the frequency of adding, removing, or changing those members is currently very high. Here is how I would implement such a class using X-Macros:

C++:
  1. #include <string>
  2. #include <iostream>
  3.  
  4. #define assertEquals(exp1, exp2) \
  5.         _assertEquals(__LINE__, exp1, exp2);
  6.  
  7. namespace
  8. {
  9.  
  10. // Implement a minimal testing framework
  11. int gErrorCount = 0;
  12.    
  13. template<class T, class U>
  14. void _assertEquals(int line, T exp1, U exp2)
  15. {
  16.     if (exp1!=exp2)
  17.     {
  18.         std::cout <<"Error: Line "
  19.                   <<line <<"; "
  20.                   <<exp1 <<"!="  <<exp2
  21.                   <<std::endl;
  22.         gErrorCount++;
  23.     }
  24. }
  25.  
  26. // (1) The code generation macro. Each X-Macro therein
  27. // receives the type and the name of the new member
  28.  
  29. #define GENERATE_PROPS          \
  30. X(int, IntValue1)               \
  31. X(std::string, StringValue1)    \
  32. X(float, FloatValue1)           \
  33. X(int, IntValue2)
  34.  
  35.  
  36. // (2) This is the class that will hold all our members
  37.  
  38. class Properties
  39. {   
  40. public:
  41. // (3) The constructor: Set all members to their default
  42. #define X(tp, name) m_##name = tp();
  43.     Properties()
  44.     {
  45.         GENERATE_PROPS
  46.     }
  47. #undef X
  48.  
  49. // (4) The getters, setters and members themselves
  50.  
  51. #define X(tp, name)                                 \
  52. public:                                             \
  53.     const tp& get##name() const {return m_##name;}  \
  54.     void set##name(const tp& in##name)              \
  55.                         {m_##name = in##name;}      \
  56. private: tp m_##name;
  57.     GENERATE_PROPS
  58. #undef X
  59.    
  60. };
  61.  
  62. } // unnamed namespace
  63.  
  64.    
  65. int main()
  66. {
  67.     Properties p;
  68.  
  69.     // (5) Test some members
  70.     assertEquals(p.getIntValue1(), 0);
  71.     p.setIntValue1(10);
  72.     assertEquals(p.getIntValue1(), 10);
  73.  
  74.     assertEquals(p.getStringValue1(), "");
  75.     p.setStringValue1("test1");
  76.     assertEquals(p.getStringValue1(), "test1");
  77.  
  78.     if (!gErrorCount)
  79.     {
  80.         std::cout <<"All tests passed" <<std::endl;
  81.     }
  82.  
  83.     return gErrorCount;
  84. }


In this more elaborate example, X-Macros are used to generate a considerable amount of code. All members of a class Properties are defined, as well as getters and setters for them. The code that initializes all members to their type-default is also generated within the constructor. Since I got tired of manually checking the correctness of the output, I also introduced a minimal testing "framework" in this example - assertEquals will report an error if it's two arguments are not equal. Before the program terminates, it will report any of these discrepancies.
Now adding a new member to Properties in the above code is simply a matter of adding a line to the generation macro at (1), thus completely eliminating the need to hunt around in the code for places that need to be adapted or expanded (or even reduced!).

By the way, X-Macros can of course also be used in tests. Testing "some" of the members (see (5)) is not satisfactory, neither is adding a new line in the test whenever a new member is defined (precisely what we want to avoid for the production code!). One way of automatically testing the correctness of the generated code is adding a test value as the third parameter to the X-Macro. The generation macro would then look like so:

C++:
  1. #define GENERATE_PROPS                          \
  2. X(int, IntValue1, 100)                          \
  3. X(std::string, StringValue1, "testValue1")      \
  4. X(float, FloatValue1, 200.5f)                   \
  5. X(int, IntValue2, 200)


Now of course all the places where X is expanded have to be adapted, to include the new parameter. In the code we have seen so far, this parameter is ignored. However, in the test, we can do something like this:

C++:
  1. int main()
  2. {
  3.     Properties p;
  4.  
  5.     // (5) Test all properties
  6. #define X(tp, name, tv) \
  7.     assertEquals(p.get##name(), tp());  \
  8.     p.set##name(tv);                    \
  9.     assertEquals(p.get##name(), tv)
  10.     GENERATE_PROPS
  11. #undef X
  12.  
  13.     if (!gErrorCount)
  14.     {
  15.         std::cout <<"All tests passed" <<std::endl;
  16.    }
  17.  
  18.     return gErrorCount;
  19. }


Now, (5) expands to code that first tests the default value of a member, then sets it's value to the test value given in the third parameter (tv) of our X-Macro, and then tests that the getter returns the same value. No new code has to be added here when a new member is defined.

A few words of warning

As always, there is a flipside of the coin. X-Macros degrade readability of cour code. When introducing an X-Macro, it is well worth spending some time figuring out how to keep the code that is eventually in an X-Macro as short as possible. Sometimes one jumps the gun and wraps too much code into it, while some static elements could still be extracted. Long macros make it hard to debug the code within them. On the other hand, do you see a reason to ever need to debug the Property class above, assuming that it's feature set will not change?

I have put X-Macros to great use in great code, and I encourage you to do the same, and to discover many more uses for this surprisingly simple yet effective technique.

You can download the code used in the examples here:
XMacros_Enum.cpp
XMacros_Properties.cpp

6 Responses to “X-Macros”

  1. vlummi Says:

    just for completeness, i found this link..: http://en.wikipedia.org/wiki/C_preprocessor#X-Macros

  2. SeB Says:

    Hello,

    Is it possible to use X-macros with two dimensions ? If I have two tables :
    X.def : X(x1) , X(x2), …., X(xm)
    Y.def : Y(y1) , Y(y2), …., Y(yn)
    can I automatically build an enum {x1_y1, x1_y2,…, x1_yn, x2_y1, x2_y2, ……….., xm_yn} ?

    Thanks,
    SeB

  3. Wolfgang Says:

    You could use two parameters, by defining something like

    #define GENERATE_IDS    \
        X(x1, "x1")Y(y1, "y1")    \
        X(x2, "x2")Y(y2, "y2")   \
        X(x3, "x3")Y(y3, "y3")
    

    And then do

    #define X(id, idString) id
    #define Y(id, idString) _id,
    

    before expanding the macro for the enum.
    But that would just give you x1_y1, x2_y2, x3_y3 etc.

    So, in short, beats me :)

  4. SeB Says:

    Thanks for your reply, Wolfgang.
    Unfortunately I need to use the X.def and Y.def files which are used by others parts of the code.

    I am trying to find out a way for sourcing a .def file multiple times from another .def file.
    That might be done with variable arguments length macros, but these are not supported by all compilers and are not so familiar to me.

    I’ll let know here if I find a reasonable solution.

    SeB.

  5. quizzical Says:

    Traditionally, the class prototype is defined in a header file, while the class implementation is defined in a .cpp file. Using XMacro-Properties.cpp as our example, is there a way to write the prototype for Properties in a header, while implementing elsewhere:

    define X(tp, name)

    const tp& Properties::get##name() const {return m_##name;}  \
    void Properties:: set##name(const tp& in##name)  \
    

    ???????

    Many thanks!

  6. Wolfgang Says:

    Sure, I just compressed the code into one file to make it easier to present. There is nothing that keeps from splitting up the code into more files, if you are so inclined. You just redefine the big X accordingly, to generate a class declaration in the header, and the implementation in the cpp file ;)

Leave a Reply