Smart Enumerations

Motivation

I came across a post a while back, written by David Saltares about higher order macros in C++ and avoiding writing repetitive code.I was a bit skeptical, but sure enough, it was a good post and I learned a lot about macros from it. You can find it here, but I’ll be covering the gist of it here as well.

The two golden rules that I’ve followed have been to avoid using macros, and to avoid repeating yourself. I know everyone agrees on the latter, but opinions on macros vary, since they can be incredibly powerful, but can also disloyally wreak havoc upon your code. After reading David’s post I still feel that macros are unsafe, but he does make a very valid case on their use.

I did feel like his post fell a tad short, since it didn’t address object oriented programming, and that’s a big part of C++. So, I got to fiddling around a bit, and brought the fabled SmartEnum to objects! I hope you enjoy, and find this a bit useful in your own endeavors.

As a tip, don’t play around with new ideas for macros in a large project. Create a temp directory and work in there. I made that mistake, and I think I may have lost a year off of my life.

A Quick Rundown of Macros

For the uninitiated, macros in C and C++ are operations that are run by the preprocessor, before code is compiled. You likely have seen this in C code in the form of `#define PI 3.14` or something similar, for constants. This will just find every occurrence of `PI` and verbatim replace it with it’s definition. This can be useful, but is also rather trivial.

A higher order macro will take parameters, and replace itself with the parameters substituted into the macros body. Here’s a simple example.

  #define STRINGIZE(data) \
  std::string(data)

  // Prints 'foo' to std::cout
  cout << STRINGIZE("foo");

There are two operators you should know about in a macro expansion. * `#` means to ‘stringify’ something, so `STRINGIFY(x) #x` would translate `STRINGIFY(foo)` into `”foo”`. * `##` means to concatenate two tokens together. so `#CONCAT(x, y) x##y` would return the token `xy`. Macros can also take other macros as arguments.

This occurs in passes. Imagine we had a macro `FOO(m) m(bar) m(zed)` and it was passed the macro `STRINGIFY(x) #x`. On it’s first pass, we are left with `STRINGIFY(bar) STRINGIFY(zed)`, then the preprocessor takes a second pass and evaluates stringify. > Note that with macros, you can accidentally mangle existing names.

Macros take precedence, so if you define `FOO(class,..)` the parameter named class takes precedence over the keyword class. This is very bad, and tripped me up at first. Be sure to not name your macro parameters in ways that overwrite existing keywords.

If you are curious to see how your macro is being expanded, try `g++ -E source.cpp > mac.txt`. This will give you the macro expanded version of your code.

SmartEnum: The non-object oriented way


Here the gist of the SmartEnum macros. This is a C++ file, but would port to C with nearly no effort at all.

What is happening here is we are defining in our code a sort of ‘payload’ macro, which we then pass off to `DEFINE_SMARTENUM` then undefine.
This is how to define the data that is within a SmartEnum.
It seems to be a round about way of defining code, but it’s the most efficient; for those keen of eye, you probably already noticed our payload is a higher order macro!

The way we define our payload allows us to then use other macros to extract data from it.
See these two macros in our code:

  #define SMARTENUM_VALUE(typeName, value) \
  e##typeName##value,

  #define SMARTENUM_STRING(typeName, value) \
  #value,

These two functions take the data defined in one ‘unit’ of our payload, and return a value.
The beauty of this is that we can call `payload(MACRO)` and our payload applies the macro to each of its units.
This allows us to define a payload of data in one place, and use it across multiple definitions!

#define SMARTENUM_DEFINE_ENUM(typeName, values) \
  enum typeName { values(SMARTENUM_VALUE) e##typeName##Count, };

  #define SMARTENUM_DEFINE_NAMES(typeName, values) \
  const char* typeName##Array[] = { values(SMARTENUM_STRING) };

Here’s two macros, using the same data in a very different way.
You can start to see the benefit to using a macro payload here rather than managing declarations by hand.
Not only are we cutting the time it would take to manage our data roughly in half, but also both ensuring consistency within our enumeration and string array.
Also, we are cutting down on the size of our source file by keeping this data all defined in one place.

Conveniently, each enumerated value will return it’s string when indexing `typename##Array[]`, however there’s no good way to go back.
Also, for portability’s sake we shouldn’t make ourselves resort to manually accessing an array all the time.

Luckily, we have more macros to the rescue!

  #define SMARTENUM_DEFINE_GET_STRING_FROM_VALUE(typeName) \
  const char* getStringFrom##typeName(enum typeName enumValue) \
  { \
  return typeName##Array[enumValue]; \
  }

  #define SMARTENUM_DEFINE_GET_VALUE_FROM_STRING(typeName, name) \
  enum typeName get##typeName##FromString(const char* str) \
  { \
  for (int i = 0; i < e##typeName##_Count; ++i) \
  if (!strcmp(typeName##Array[i], str)) \
  return (i); \
  return e##typeName##_Count; \
  }

What’s cool about these macros is that they are actually defining functions in our source code.
That’s entirely valid! macros can do that.
Ignoring the ugly `##` operators scattered throughout, you can see these functions are rather simple.
The former just wraps an array index operation, while the latter iterates over our string array until we find a match.

Now, to define a SmartEnum, we’d have to make four macro calls in order.
That’s a huge pain, since we really will be declaring all this data at once anyway.
Luckily, macros can contain macros, so let’s wrap up all those pesky calls like so:

  #define DEFINE_SMARTENUM(name, enumList) \
  SMARTENUM_DEFINE_ENUM(name, enumList) \
  SMARTENUM_DEFINE_NAMES(name, enumList) \
  SMARTENUM_DEFINE_GET_STRING_FROM_VALUE(name) \
  SMARTENUM_DEFINE_GET_VALUE_FROM_STRING(name, enumList)

Beautiful, now we can make a SmartEnum with one call to `DEFINE_SMARTENUM`.
This is my one big deviation from David’s approach, but I feel like it’s cleaner than making multiple calls to achieve one result.

That’s it for the non-object oriented approach!
If you went and read David’s post, this is where I stop paraphrasing what he did.
Now let’s get objective with it! (I’m sorry… it’s late and I had to get at least one pun into this)

SmartEnum: Now with 100% more objects

This code makes some changes from what we defined before, but the spirit is roughly the same.
Here’s the gist of it:

Our ways of extracting data from the payload are entirely unchanged, so let’s not bother with them.

  #define SMARTENUM_DEFINE_ENUM(typeName, values) \
  enum typeName { values(SMARTENUM_VALUE) e##typeName##_Count, };

  #define SMARTENUM_DEFINE_NAMES(typeName, values) \
  static const std::vector typeName##Vector;

Of our next two functions, the definition of the enumeration is entirely unchanged as well.
Why?
Well, since in C++ a enumeration defined in a class is scoped to the class.
Its already ‘static’ in a sense, we don’t need to change it.

Our `DEFINE_NAMES` macro has definitely changed though.
It’s declared `static const`, and isn’t initialized.
It makes little sense to have a unique copy of the enumeration names, thus the `static`, and we don’t want its value to change, thus the `const`.
We can’t initialize members in the class, so we have to put the initialization of this data on hold for now, and trust we will initialize it later.

  #define SMARTENUM_DEFINE_GET_STRING_FROM_VALUE(typeName) \
  static std::string getStringFrom##typeName(typeName enumValue) \
  { \
  return typeName##Vector[enumValue]; \
  }

  #define SMARTENUM_DEFINE_GET_VALUE_FROM_STRING(typeName, name) \
  static typeName get##typeName##FromString(std::string str) \
  { \
  for (int i = 0; i < e##typeName##_Count; ++i) \
  if (typeName##Vector[i].compare(str) == 0) \
  return ((typeName) i); \
  return e##typeName##_Count; \
  }

We have made our functions to translate between enumerations and string names static as well, although they remain unchanged otherwise.
Again, these are static because it makes no sense to have each instance of the class have it’s own attached method for conversions between static data.

You can see, largely the changes that have been made were to allow for these enumeration definitions to be used within classes.
The data itself is still static in nature, just like before.
However in any large project, defining enumerations without them being contained in some scope is folly.
It makes much more sense to have a `Car` class that statically contains enumerations and their string representations than to have `Car`, and an outside enumeration of car types.

  #define DEFINE_SMARTENUM(class, name, enumList) \
  SMARTENUM_DEFINE_ENUM(name, enumList) \
  SMARTENUM_DEFINE_NAMES(name, enumList) \
  SMARTENUM_DEFINE_GET_STRING_FROM_VALUE(name) \
  SMARTENUM_DEFINE_GET_VALUE_FROM_STRING(name, enumList)

  #define SMARTENUM_INIT(class, name, values) \
  const std::vector class::name##Vector({ values(SMARTENUM_STRING)});

Our `DEFINE_SMARTENUM` macro hasn’t changed, however we do have one additional macro now that we need to call.
Because we can’t define static data, we need an ‘init’ macro to initialize the class, and assign the static data a value.
In this case, it wraps a call that would be rather trivial, but could become increasingly complicated (and thus necessary) in larger projects.

To define a SmartEnum, we’d do something like this:

  class carClass {
  public:
  #define CAR_LIST(m) \
  m(Car, Toyota) \
  m(Car, Honda) \
  m(Car, Volvo)
  DEFINE_SMARTENUM(carClass, CarEnum, CAR_LIST)
  };
  SMARTENUM_INIT(carClass, CarEnum, CAR_LIST)
  #undef CAR_LIST

Conclusion


David’s article and my subsequent playing with it in C++ definitely expanded my view of macros. Before, I never would have considered using them in my own code unless necessary, but I’ve definitely come to see the value of them. They definitely can be dangerous, but with proper documentation and construction, they certainly can be a powerful tool.

Leave a Reply

Your email address will not be published. Required fields are marked *