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
- Introduction
- Using Enumerations
- Using Flags/Enumsets
- Integrating Existing Enum Classes
- Customizations
- Troubleshooting
- Modifying the Source Code
- 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:
PP_MACRO_ENUM_CLASS_EX(
(DemoEnum)(SubNamespace), EType_B, (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:
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:
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):
bool Enum::IsValid(int inProbableEnum);
vector<TEnum> Enum::GetValues();
vector<string> Enum::GetNames();
string Enum::ToString(TEnum inEnum);
TEnum Enum::FromString(string inString);
bool Enum::TryParse(string inProbableEnum, TEnum* outValue);
bool Enum::TryParse(int inProbableEnum, TEnum* outValue);
TEnum Enum::FromInt(int inProbableEnum);
And for flags:
bool AreValid(int inProbableEnum);
bool IsSet(TEnum inFlags, TEnum inFlagToCheck);
bool IsAnySet(TEnum inFlags, TEnum inFlagsToCheck);
bool AreAllSet(TEnum inFlags, TEnum inFlagsToCheck);
vector<TEnum> Decompose(TEnum inFlags);
vector<TEnum> GetValues();
bool TryParse(int inProbableEnum, TEnum* outValue);
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:
#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:
#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:
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:
PP_MACRO_ENUM_CLASS_EX(
(System)(Drawing), EColor, (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:
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:
BOOST_FOREACH(string colorName, Enum::GetNames<EColor>())
{
cout << colorName << " = "
<< (int)Enum::FromString<EColor>(colorName) << endl;
}
and similarly through all values:
BOOST_FOREACH(EColor colorValue, Enum::GetValues<EColor>())
{
cout << Enum::ToString(colorValue) << " = "
<< (int)colorValue << endl;
}
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):
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:
cout << Enum::IsValid<EColor>(1); cout << Enum::IsValid<EColor>(2); cout << Enum::IsValid<EColor>(3); cout << Enum::IsValid<EColor>(4); cout << Enum::IsValid<EColor>(5); cout << Enum::IsValid<EColor>(6); cout << Enum::IsValid<EColor>(7); cout << Enum::IsValid<EColor>(8); cout << Enum::IsValid<EColor>(11);
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:
PP_MACRO_ENUM_CLASS_FLAGS(
(System)(Compiler), EOpMode, 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:
EOpMode opMode = Flags::Of(EOpMode::Compile, EOpMode::Link, EOpMode::Execute);
BOOST_FOREACH(EOpMode step, Flags::Decompose(opMode))
{
cout << Enum::ToString(step) << " = " << (int)step << endl;
}
Flags::Of
is simply a shortcut for OR
ing 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:
BOOST_FOREACH(EColor color, Flags::GetValues<EColor>())
{
cout << Enum::ToString(color) << " = " << (int)color<< endl;
}
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:
Flags::AreValid<EColor>(1); Flags::AreValid<EColor>(2); Flags::AreValid<EColor>(8); Flags::AreValid<EColor>(3); Flags::AreValid<EColor>(11); Flags::AreValid<EColor>(10); Flags::AreValid<EColor>(9);
There are some masking methods available like AreAllSet
, IsAnySet
, IsSet
:
opMode = Flags::Of(EOpMode::Link, EOpMode::Execute);
Flags::AreAllSet<EOpMode>(opMode, EOpMode::Preprocess); Flags::AreAllSet<EOpMode>(opMode, EOpMode::Execute); Flags::AreAllSet<EOpMode>(opMode, opMode); Flags::AreAllSet<EOpMode>(opMode, Flags::Of(EOpMode::Link, EOpMode::Execute, EOpMode::Preprocess));
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:
#if !CFLAGS_ENABLE_ENUM_CLASS_WORKAROUND
namespace Some { namespace Nested { namespace Namespace {
enum class ESomeEnum
{
a_0 = 1, b_1 = 4, c_2 = 8, d_3 = 3,
};
}}}
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 typedef
s,
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:
#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:
#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!