x-macros are a great meta-programming idiom for C & C++, and I've been using them for years in my own code.

Here's an example that demonstrates why they are so useful.

An enum for dog breeds

Let's say that I want to enumerate dog breeds. I know the breeds I want to enumerate now, but I also want to be able to add more later. I also want to be able to convert the enum values to and from a human-readable string. The following demonstrates how x-macros can help make this easy.

#include <string>
#include <iostream>
using namespace std;

// 1. Define an expanding macro that encodes the enum values

#define EXPAND_ENUM_DOGS(DOG) \
    DOG(DOG_BEAGLE  , "beagle"  ) \
    DOG(DOG_DOBERMAN, "doberman") \
    DOG(DOG_PUG     , "pug"     ) \
    DOG(DOG_POODLE  , "poodle"  ) \
    DOG(DOG_TERRIER , "terrier" ) \
    DOG(DOG_BOXER   , "boxer"   )

// 2. Define a macro with syntax for a single enum value, then expand

#define CASE(_enum, _str) ,_enum
enum Dog { DOG_INVALID EXPAND_ENUM_DOGS(CASE) };
#undef CASE

string to_string(Dog dog)
{

// 3. Define a macro that will return a string for a given enum value, then expand

    #define CASE(_enum, _str) case _enum: return _str;
    switch (dog) { EXPAND_ENUM_DOGS(CASE) };
    #undef CASE
    return "DOG_INVALID";
}

Dog to_dog(string const& str)
{

// 4. Define a macro that will return an enum value for a matched string, then expand

    #define CASE(_enum, _str) if (str == _str) return Dog::_enum;    
    EXPAND_ENUM_DOGS(CASE)
    #undef CASE
    return DOG_INVALID;
}

int main()
{
    Dog my_dog = DOG_BEAGLE;

    cout << "My dog is a " << to_string(my_dog) << endl;

    Dog your_dog = DOG_INVALID;

    while (your_dog == DOG_INVALID)
    {
        cout << "What kind of dog do you have? ";
        if (string line; getline(cin, line))
        {
            your_dog = to_dog(line);
            if (your_dog == DOG_INVALID)
                cout << "I've never heard of that before..." << endl;
            else
                cout << "Ah, a '" << to_string(your_dog) << "', how wonderful!" << endl;
        }
    }

    return 0;
}

Look closely at the macro EXPAND_ENUM_DOGS, that's the important part. See that it has an input parameter. That parameter is itself ANOTHER macro with its own input parameters.

It doesn't define anything on it's own, but can be used by its input (DOG, in this example) to do something useful. This is a form of Metaprogramming. Your pre-defined data elements are encoded within the macro, which becomes the source of truth any code you may want to generate.

Take a look at the to_string function:

string to_string(Dog dog)
{
    #define CASE(_enum, _str) case _enum: return _str;
    switch (dog) { EXPAND_ENUM_DOGS(CASE) }; // <--- Pay attention to this line
    #undef CASE
    return "DOG_INVALID";
}

This is the literal expansion of EXPAND_ENUM_DOGS(CASE)

case DOG_BEAGLE: return "beagle";
case DOG_DOBERMAN: return "doberman";
case DOG_PUG: return "pug";
case DOG_POODLE: return "poodle";
case DOG_TERRIER: return "terrier";
case DOG_BOXER: return "boxer";

And again, look at the to_dog function

Dog to_dog(string const& str)
{
    #define CASE(_enum, _str) if (str == _str) return Dog::_enum;
    EXPAND_ENUM_DOGS(CASE) // <--- Pay attention to this line
    #undef CASE
    return DOG_INVALID;
}

The same macro EXPAND_ENUM_DOGS(CASE) expands differently, since it sees a new definition for CASE

if (str == "beagle") return Dog::DOG_BEAGLE;
if (str == "doberman") return Dog::DOG_DOBERMAN;
if (str == "pug") return Dog::DOG_PUG;
if (str == "poodle") return Dog::DOG_POODLE;
if (str == "terrier") return Dog::DOG_TERRIER;
if (str == "boxer") return Dog::DOG_BOXER;

The key idea is to repeat against the execution of an expanding macro. This can then be leveraged to eliminate large repeating patterns in your code, as well as guarantee correctness.

Ever since I learned this technique, I have used it extensively in my own code. It makes working with enums in C++ much more functional than the base language allows. It can also be used to implement a light-weight form of polymorphism. The performance benefits are also pretty clear, this dog breed example could have been written with a lookup table, which would have incurred an additional memory and runtime cost.

If you want more practical examples of this trick, see my code library ut, which I actively maintain on github and uses x-macros throughout (hint: look specifically at color.hpp.


Do you need a skilled programmer? I'm available for hire! I can do both front and back-end web development and I have a lot of experience writing low-level code in C & C++. Feel free to contact me at mail@jpmitchell.ca