Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VC10.0

C++11: Non-intrusive enum class with Reflection support using Metaprogramming

4.93/5 (6 votes)
15 Feb 2013MIT16 min read 52.7K   633  
The clean way for getting C++ enumerations with Reflection support.

Both projects require Boost 1.48 and the first one additionally requires the Google Testing Framework 1.6 to be installed. The first one is more like a unit test where you naturally can see most features in action, while the second one is rather a simple test project which might be easier to understand!

Table of Contents

  1. Introduction
  2. Using Enumerations
  3. Using Flags/Enumsets
  4. Integrating Existing Enum Classes
  5. Customizations
  6. Troubleshooting
  7. Modifying the Source Code
  8. Behind the Scenes

Introduction

This articles solves the Reflection issue for C++ enumerations, and especially the "enum class"es that come with C++11. The Reflection issue describes the ability to do things like "Enumerate all values", "Enumerate member strings", "Convert to/from string", "Safe casts from integer", and "Flags or so called Enum-Sets". Of course one could always do that manually, but this comes at the cost of writing a lot of useless boilerplate and code that is hard to maintain. The framework I present here is used in a large software project (mine), and will be updated regularly in case of bugs or whatever, if you or I report something...

There are already a lot of articles about this topic out there, but they all, without exception (at least not known to me), don't fulfill the most important requirement for any large scale project. That is they require you to type enumeration members twice. Some do even worse and require you using additional files for your enumerations or even utilize generators (that's about the worst it could get). If we don't take that into consideration, additionally most of them suffer from at least one or more of the following:

  • They are not using the native enumeration types
  • Are not portable (this framework is known to work on VS2010, Intel Compiler 11, CLang 3 [Linux] and their successive versions)
  • Do not support new C++11 extensions, like enum classes
  • Are not customizable, which is of high importance when considering large scale projects
  • They screw up IntelliSense (my approach is, surprisingly enough, fully supported by VS IntelliSense)
  • Are verbose to use
  • Well, probably more, but I can't remember!

So now let's take a look at how you can define a fully reflectable enum class with the framework described here:

C++
PP_MACRO_ENUM_CLASS_EX(
    (DemoEnum)(SubNamespace), // put in namespace "DemoEnum::SubNamespace"
    EType_B, // typename
    (e_0, 2), 
    (f_1, 8), 
    (g_2, 1), 
    (all, e_0 | f_1 | g_2), 
    (alias, f_1)
);

That's it. No additional code is required on your side and this is all the framework needs of you. I won't go into the details of the strange syntactical notion that is required here. In the section "Behind the scenes", you will find some pointers on how to go on if you want to understand the foundation of this framework.

Note that there is also a less verbose shortcut, I will introduce below.

When only considering the generated type, using a C++11 compiler (otherwise it will look different), the above macro expands (among a lot of other stuff) to:

C++
namespace DemoEnum {
    namespace SubNamespace
    {
        enum class EType_B
        {
            e_0 = 2, 
            f_1 = 8, 
            g_2 = 1, 
            all = e_0 | f_1 | g_2, 
            alias = f_1,
        };
    }
}

Some may be worried about the "enum class". Yes, this is C++11 and not supported by Visual Studio 2010, but it will be in VS2011. Additionally, if support for enum classes is not available, the framework will automatically pick a fallback (described later) which is more or less compatible to enum classes and a good temporary workaround (the code you write does not need to be aware of the difference when you stick to some basic rules). There is also a shortcut, which looks like this:

C++
PP_MACRO_ENUM_CLASS(
    (DemoEnum)(SubEx), 
    EType_A, 
    a_0, b_1, c_2, d_3
);

It does pretty much the same, except that it automatically assigns enumeration values, from zero to infinity, to your enumeration members. This is exactly the same as C++ does it, where "a_0" would be set to zero, "b_1" to one, and so on. If you don't need custom values, which is often the case, this shortcut will come in handy.

So what does this give us? Well, now you can use this fresh new type with a set of predefined enumeration support routines that come along with the framework (TEnum denotes a template parameter which is on demand replaced by a proper enumeration type):

C++
// is the given integer value a valid enumeration member?
bool Enum::IsValid(int inProbableEnum);

// returns a string list with all member values for a specific enumeration
vector<TEnum> Enum::GetValues();

// returns a string list with all member identifiers for a specific enumeration
vector<string> Enum::GetNames();

// looks for the enum member identifier that maps
// to the given value and returns it string representation
string Enum::ToString(TEnum inEnum);

// Safely converts the given string to an enumeration
// value by member identifier lookup (case-insensitive).
// Throws "PP_MACRO_ENUM_ARG_EXCEPTION" if conversion fails.
TEnum Enum::FromString(string inString);

bool Enum::TryParse(string inProbableEnum, TEnum* outValue);

bool Enum::TryParse(int inProbableEnum, TEnum* outValue);

// Safely converts the given int to an enumeration value by member value lookup.
// Throws "PP_MACRO_ENUM_ARG_EXCEPTION" if conversion fails.
TEnum Enum::FromInt(int inProbableEnum);

And for flags:

C++
// is the given integer value a valid flags value?
bool AreValid(int inProbableEnum);

// is the given flag "inFlagToCheck" set in "inFlags"?
bool IsSet(TEnum inFlags, TEnum inFlagToCheck);

// is any of the given flags "inFlagToCheck" set in "inFlags"?
bool IsAnySet(TEnum inFlags, TEnum inFlagsToCheck);

// are all the given flags "inFlagToCheck" set in "inFlags"?
bool AreAllSet(TEnum inFlags, TEnum inFlagsToCheck);

// returns a list of enumeration members that are set
// in "inFlags"? Useful for ToString() implementations...
vector<TEnum> Decompose(TEnum inFlags);

// returns a list of all valid flag values. Useful for FromString() implementations...
vector<TEnum> GetValues();

bool TryParse(int inProbableEnum, TEnum* outValue);

// Safely converts the given int to a flags value by member value lookup.
// Throws "PP_MACRO_ENUM_ARG_EXCEPTION" if conversion fails.
TEnum FromInt(int inProbableEnum);

Since enumerations are of fixed size, naturally the time complexity of all methods is O(1). But even if we want to be honest, the practical constant is quite low, heavily depending on compiler optimizations. If not properly supported, especially the enumerative routines could suffer considerable performance hit, which you still won't notice in most applications. If this is still a problem for you, either cache the results or replace the enumerative containers with std::array. For the latter, you have to understand what's going on in the sources, though. Maybe I will do that myself at some other time and post an update.

Using Enumerations

First let's look at how to include this framework in your code. I will assume a clean Visual Studio 2010 SP1 installation and a fresh new C++ console application project. Now add the root directory of your boost 1.48 (lower versions might work as well) installation as an additional include path. Of course you will also need access to the framework files that come with this project. These are:

C++
#include "EnumFramework_Config.h"
#include "EnumFramework_Magic.h"
#include "EnumFramework_Custom.h"

The first one provides some general configuration (see section "Customization"). The second is the scary internal stuff you should not manipulate unless you know what you are doing. The third one contains usual C++ code implementing the support routines. This is probably the most interesting one if you want to customize this thing (see section "Modifying the Source Code"). You need to include some other dependencies, before including the framework headers! If you setup the boost include directory properly, everything shall be alright and will look similar to this:

C++
#include <stdio.h>
#include "include/gtest/gtest.h"
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <assert.h>

#include <boost/foreach.hpp>
#include <boost/unordered_map.hpp>
#include <boost/assign/list_of.hpp>
#include <boost/../libs/unordered/examples/case_insensitive.hpp>

#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/tuple/to_list.hpp>
#include <boost/preprocessor/list/for_each.hpp>
#include <boost/preprocessor/facilities/empty.hpp>
#include <boost/preprocessor/facilities/expand.hpp>
#include <boost/preprocessor/selection/max.hpp>
#include <boost/preprocessor/tuple/elem.hpp>
#include <boost/preprocessor/punctuation/comma_if.hpp>
#include <boost/preprocessor/stringize.hpp>
#include <boost/preprocessor/seq/for_each.hpp>
#include <boost/preprocessor/control/expr_if.hpp>
#include <boost/preprocessor/control/iif.hpp>
#include <boost/preprocessor/logical/or.hpp>

#include "EnumFramework_Config.h"
#include "EnumFramework_Magic.h"
#include "EnumFramework_Custom.h"

Now you can start defining enums as you please.

Attention: One rule to keep in mind is that you shall always place the macros in the root namespace! Otherwise it won't work. For example:

C++
namespace MyNamespace
{
    PP_MACRO_ENUM_CLASS(
        (DemoEnum)(SubEx), 
        EType_A, 
        a_0, b_1, c_2, d_3
    );
}

won't place "EType_A" in the namespace "MyNamespace::DemoEnum::SubEx". Instead it will smash a pile of errors at you!

I am confident that this is a restriction imposed by my limited meta-programming skills. After all I am in serious C++ development for only a few weeks (C#, Java, and "C like C++" previously)... A more experienced metaprogrammer should be able to remove this restriction, at least if the language specification allows it (I am not sure about that). The main issue I have here is that I need to define namespaces relative to the global namespace and this seems to be forbidden inside of a subnamespace. To solve this, one would probably require a lot more sophisticated design I would not be willing to afford.

Also note that if you plan on later supporting C++11, you should regularly compile your code with a C++11 compiler and the CFLAGS_ENABLE_ENUM_CLASS_WORKAROUND macro set to 0, preferably as a compiler option. This will cause the EnumFramework to generate enum classes instead of enum hacks ;). While both are compatible to some extend, you will still have to fix certain compiler errors in either case to get them both to work smoothly for the same code (they each require additional casts or template parameters here and there).

Now that everything is setup, you may be interested in some actual code samples of the support routines. I will just guide you through with small snippets, since they shall be rather self explaining. For a full demonstration, look at the sample project which uses most of the functionality for the sake of testing it.

First, we declare a small enumeration:

C++
PP_MACRO_ENUM_CLASS_EX(
    (System)(Drawing), // put in namespace "System::Drawing"
    EColor, // typename
    (Black, 2), 
    (White, 8), 
    (Red, 1), 
    (All, Black | Red | White), 
    (SimilarWhite, White)
);

From now on, consider all code (for the rest of this section) to be contained in the following "main" function:

C++
int main(int argc, char* argv[])
{
    using namespace std;
    using namespace System::Drawing;
    using namespace System::Compiler;
    using namespace MetaEnumerations;

    return 0;
}

Now we can loop through names like:

C++
BOOST_FOREACH(string colorName, Enum::GetNames<EColor>())
{
    cout << colorName << " = " 
         << (int)Enum::FromString<EColor>(colorName) << endl;
}

/* Output of loop:
    Black = 2
    White = 8
    Red = 1
    All = 11
    SimilarWhite = 8
*/

and similarly through all values:

C++
BOOST_FOREACH(EColor colorValue, Enum::GetValues<EColor>())
{
    cout << Enum::ToString(colorValue) << " = " 
         << (int)colorValue << endl;
}

/* Output of loop:
    Black = 2
    SimilarWhite = 8
    Red = 1
    All = 11
    SimilarWhite = 8
*/

Notice the use of "SimilarWhite" instead of the expected "White". This is because there is no way to distinguish between them, as they have the same numeric value.

You may also read values from string input (case-insensitive in default configuration):

C++
string colorName;
EColor colorValue;

do{
    cout << endl << "Enter a valid color: ";
    cin >> colorName;
}
while(!Enum::TryParse(colorName, &colorValue));

cout << endl << "You have entered \"" << Enum::ToString(colorValue) << "\"!" << endl;

As you may know, numeric values can be casted to enumerations (even with enum classes) and so the expression (EColor)443 is correct C++ code, but inherently wrong! With the following, you can now (easily) validate such values:

C++
cout << Enum::IsValid<EColor>(1); // true
cout << Enum::IsValid<EColor>(2); // true
cout << Enum::IsValid<EColor>(3); // false
cout << Enum::IsValid<EColor>(4); // false
cout << Enum::IsValid<EColor>(5); // false
cout << Enum::IsValid<EColor>(6); // false
cout << Enum::IsValid<EColor>(7); // false
cout << Enum::IsValid<EColor>(8); // true
cout << Enum::IsValid<EColor>(11); // true 

Be aware of that all conversion functions which take either an int or an enum and are not prefixed with "Is" or "Try" will throw an exception if you pass an invalid enumeration value!

There is not much more to know about enums right now.

Using Flags/Enumsets

Instead we will take a look at flags, or enumsets. In contrast to enum sets, flags are intended to hold more than one enumeration member at a time while still fitting into the same space. This is done by assigning every enumeration member a power of two. There is a special macro included for this purpose:

C++
PP_MACRO_ENUM_CLASS_FLAGS(
    (System)(Compiler), // put in namespace "System::Compiler"
    EOpMode, // typename
    Preprocess,
    Compile, 
    Link, 
    Optimize, 
    Execute
);

This will define an enumeration EOpMode where every member has precisely one bit set and thus they can all be combined as you please. Of course, you could thread any enumeration as flags, but only the ones with one bit per member usually make sense, unless you want to specify masks. If you don't want to interoperate with native code, putting masks into the enum is usually a bad idea and this why there is no special construct included. If you want to use masks, you will have to specify your enum using the more verbose PP_MACRO_ENUM_CLASS_EX.

You can now easily decompose such flags, which is useful for validation, specific tasks that need to be invoked for specific flags set, converting flags to string or whatever:

C++
EOpMode opMode = Flags::Of(EOpMode::Compile, EOpMode::Link, EOpMode::Execute);

BOOST_FOREACH(EOpMode step, Flags::Decompose(opMode))
{
    cout << Enum::ToString(step) << " = " << (int)step << endl;
}

/* Output of loop:
    Compile = 2 
    Link = 4 
    Execute = 16
*/

Flags::Of is simply a shortcut for ORing enum values; especially since enum classes do not support this (for good reason, since it usually only makes sense on flags) without casting to int, this method comes in handy.

Additionally, you may get all flag values from an enum value. Only enum members which have precisely one bit set will be included in this enumeration:

C++
BOOST_FOREACH(EColor color, Flags::GetValues<EColor>())
{
    cout << Enum::ToString(color) << " = " << (int)color<< endl;
}

/* Output of loop:
    Red = 1
    Black = 2
    SimilarWhite = 8
*/

As you can see, EColor::All is not included, since it is composed of more than one bit.

Flags suffer from the same problem as enumerations, which is (EOpMode)-456 is a valid expression but just makes no sense at all. For flags, you need a special function for validation, since their value does usually not map to a member:

C++
Flags::AreValid<EColor>(1); // true
Flags::AreValid<EColor>(2); // true 
Flags::AreValid<EColor>(8); // true
Flags::AreValid<EColor>(3); // true
Flags::AreValid<EColor>(11); // true
Flags::AreValid<EColor>(10); // true
Flags::AreValid<EColor>(9); // true
// false for everything else...

There are some masking methods available like AreAllSet, IsAnySet, IsSet:

C++
opMode = Flags::Of(EOpMode::Link, EOpMode::Execute);

Flags::AreAllSet<EOpMode>(opMode, EOpMode::Preprocess); // false
Flags::AreAllSet<EOpMode>(opMode, EOpMode::Execute); // false
Flags::AreAllSet<EOpMode>(opMode, opMode); // true
Flags::AreAllSet<EOpMode>(opMode, Flags::Of(EOpMode::Link, EOpMode::Execute, EOpMode::Preprocess)); // true

Well, there are more methods available but they are kinda self-explaining. Of course, there are plenty of more methods to think of, like validating flags based on conditional masks which really check if a flag is valid based on sophisticated rules. Feel free to add this functionality!

Integrating Existing Enum Classes

I am not sure if this is required at all, since support is only there for enum classes anyway and there shouldn't be too much code around already using them. Unfortunately, usual C++ enums can not be supported. You can easily integrate existing enum classes, but you will have to retype the members in that case:

C++
#if !CFLAGS_ENABLE_ENUM_CLASS_WORKAROUND

    // consider the following to be your existing enum
    namespace Some { namespace Nested { namespace Namespace {
        enum class ESomeEnum
        {
            a_0 = 1, b_1 = 4, c_2 = 8, d_3 = 3,
        };
    }}}

    // now it is simple to import it:
    PP_MACRO_ENUM_CLASS_IMP((Some)(Nested)(Namespace), ESomeEnum, a_0, b_1, c_2, d_3)
#endif

The values will automatically be retrieved from your original enum class and you do not need to type them again (it is also not supported). The above macro does not introduce any new type to work with. Instead it just skips the type generation and only dumps out the supporting code for making your enumeration work with the Reflection provided here! The CFLAGS_ENABLE_ENUM_CLASS_WORKAROUND switch is important, since the above definition only makes sense when enum classes are used and supported (C++11 standard).

Customizations

The framework allows for some configuration and comes with macro-placeholders for most of the used datatypes and namespaces (I chose macros over typedefs, since they may cause trouble with templates especially since I can not rely on the template aliases introduced in C++11 due to lack of support). So it shouldn't be too hard for you to adjust all important properties to suite your needs. You can either change the macro in the header file "EnumFramework_Config.h" or define them before including this header, in which case it will take your macro instead of redefining it.

In the following text, I will give a short explanation of what you can configure. First, look at all the macros that are used in configuration:

C++
#define PP_MACRO_STD_VECTOR std::vector
#define PP_MACRO_STD_STRING std::string
#define PP_MACRO_STD_UNORDERED_MAP boost::unordered_map
#define PP_MACRO_STD_IEQUAL_TO hash_examples::iequal_to
#define PP_MACRO_STD_IHASH hash_examples::ihash
#define PP_MACRO_ENUM_ARG_EXCEPTION std::exception
#define PP_MACRO_STD_MAKE_PAIR std::make_pair
#define PP_MACRO_METAENUM_NAMESPACE (MetaEnumerations)
#define PP_MACRO_METAENUM_ENUM_NAMESPACE (MetaEnumerations)(Enum)
#define PP_MACRO_METAENUM_FLAGS_NAMESPACE (MetaEnumerations)(Flags)

The first six macros denote the respective types that will be used in the framework. This is usually helpful, since not everyone is using STD types or BOOST, respectively. So they provide a convenient way of making the framework compatible with your own types.

The last three macros denote the namespaces where the EnumFramework will put its internal stuff as well as the names for Enum support routines and Flags support routines. If you change the latter two, all the above code examples will probably not compile anymore ;). For example, if you do:

C++
#define PP_MACRO_METAENUM_ENUM_NAMESPACE (System)(Enumeration)

You will no longer find methods like Enum::ToString() in the MetaEnumerations namespace but rather in the System namespace and have to use Enumeration::ToString() instead, provided that you've inlined System previously.

Additionally, there is a CFLAGS_ENABLE_ENUM_CLASS_WORKAROUND macro. It currently evaluates to one, because the standard detection for C++11 doesn't seem to work these days but maybe in a few years ;). So for now, if you want to enable C++11 support, that is enum classes, you have to manually set this flag to zero before including any of the framework specific header files.

If you still want to dig deeper, continue in section "Modifying the source code".

Troubleshooting

Make sure you are using the correct syntax. If you type in something wrong when declaring your enumeration, you will most likely get a massive amount of unrelated errors which reveal as much about the real cause as you staring at a can of meat... So be careful. Another thing that might cause trouble is when your enum members are macro names. While this also won't work in most cases when using the raw C++ type, the difference is again that you won't get useful error information, but just a pile of scary junk.

In case of Linux, there is one unsolved problem, the Linker! It seems to take its job a little bit too seriously and does not permit static template data members to be redefined in multiple compilation units. This is a serious problem and the only way I found to solve this is to create just one CPP file and include all your other CPP files in it. Then it will work, provided that your code supports that kind of compilation (especially if you didn't plan on doing this, it might actually cause some trouble). If anyone has a fix for this, I would really appreciate it, since I also need Linux support myself... Currently, I can't think of something other than using static template data members to do the job (at least not without typing enum members twice and this is an absolute no go for me; then I'd rather go not using enums at all). Well, to be honest, I have something in mind that could work, but since I am not in desperate need of a Linux fix, I won't look into it right now as there are bigger fish to fry. It is based on introducing another macro that just receives the namespace and enumeration name (this is something I could agree on, since you don't have to synchronize members, and enum names are unlikely to change over time, since it would cause a massive need for refactoring). This macro would now be declared only in one compilation unit per enumeration and will act as a well named proxy for providing the Reflection data.

Modifying the Source Code

If you are not to be scared easily, take a look at the source code (which is really ugly, but there is not much you can do about it; in the end, it provides the means for getting your code cleaner at least), instead of just working with the macros. I will give you a few pointers on where to start and what you can change without the need of understanding how this framework is working ;).

The most important and trivial change is adding new methods. For this, you have to add a new public static method for the class template<class TEnum> struct EnumSupport located in the file "EnumFramework_Custom.h". Additionally, you should create an alias either in the Enum or Flags namespace located below the EnumSupport class definition. There you just create an alias like the others located in there. I think it is pretty obvious if you look closer. Just let you guide by the existing methods.

Another thing that you may want to change is the compatibility workaround that is used when no enum classes are available. It is located at the bottom of the file and starts with struct TEnum. I can't tell you what to do here, you will know what you want to change to make it suit your needs.

The example application uses the Google Testing Framework and comes with a bunch of essential tests, validating the whole concept of reflective enumeration. If you change something, you shall always make sure that the test cases run through properly and the code does compile with MSVC 2010 and CLang 3 (supported by Apple). These are the most important compilers in my opinion (they couldn't be more disjoint), and if you support them both, especially thanks to the high degree of standard conformance of CLang, your code will most likely also compile with any other major compiler, at most with slight modifications. Also note that CLang is an amazing technology. Even though it is not really ready for productive development, due to lack of good tools and debuggers, it is the way to go for any future effort on C++. It provides outstanding error reporting mechanisms, without whcih this project would not have been possible! For example, in Visual Studio, you get something along the lines of "Unexpected ',' at the end of identifier". And if you double click, it shows you the macro from which this error originated. Also, the command line output shows no more information even. Yeah, nice. Depending on the macro, this may actually mean hours of trial and error code-changes until you find out the real reason. Time enough to install the CLang compiler and let it give you more insights. It will generate a full macro-expansion and template instantiation trace for the specific error and underline in each trace step which expression was replaced with what and at which source code line this trace step takes place (even with source code dump of the current line). And it shows useful error messages that pinpoint the actual problem instead of some random weird garbage. CLang really is something evolved and the people have learned a lot from the failures of GCC (at least when it comes to C++).

Behind the Scenes

If you wan't to know more, just read some of the good docs about "Preprocessor Metaprogramming" and also C++ templates. What's done here is simply abusing the preprocessor and template instantiation for code generation or let's say some sort of domain specific language; in that case, a language to define reflective enums. My time is somewhat up right now, and also I think it wouldn't make much sense to add this kind of information to this article, since the stuff discussed before was really straightforward and now it would get really nasty ;), but also a lot more interesting I have to admit. But this article was about how to use this framework, not how to re-implement it! 

License

This article, along with any associated source code and files, is licensed under The MIT License