Introduction
.NET programmers are mostly familiar with Reflection. Reflection is a big topic. Here I just address Reflection on Enums. In this article, I'll represent a C++ [template + macro] solution to bring .NET's Reflection ability on Enum into the C++ world. The utility is implemented as a template class.
The code is checked by:
- Comeau C/C++ 4.3.10.1 (Oct 6 2008 11:28:09) for ONLINE_EVALUATION_BETA2; only one warning about use of the non-standard
stricmp
- VC2003 (/W4 /WX)
- VC2008 (/W4 /WX)
- gcc -c -Wall -xc++ (version gcc (GCC) 3.3.3 (Cygwin special))
- PC-Lint online(9.00b)
Background
While .NET/C# is getting more and more popular, C++ is still unavoidable for most programmers in some scenarios. With .NET/C#, we can use enum
as a true type and get the following operations on it:
ToString()
Parse(Type, String)
, Parse(Type, String, Boolean)
IsDefined( Type enumType, Object value)
GetValues()
GetNames()
My working project is a hybrid project mixed with C# and C++ code. When I switched to C++ code, suddenly I lost a lot of the productivity in C#. I'm not here to compare the languages, this is just my personal feeling. I'm very interested in the question: Why? And how can I bring the productivity in C# to C++? There are many factors I found practical to approach these goals: avoid pointers if possible, use RAII/smart pointers, use STL containers instead of native arrays, reuse existing libraries, etc. Most of the rules are not new, and have already been discussed in <<Effective C++>> <<More Effective C++>> etc., many years ago. But I cannot find a solution to make me apply operations on enum
in C# to C++.
The motivation is the log system in our project. To fine control every log item, we define a LogLevel
enum, and a LogBy
enum as follows:
enum LogLevel{ Error, Info }
enum LogBy { ZhaoRuFei, Wang, Zhang }
Of course, we want to also log the log level and the author as a string
in the log file. Now the first question arises: How do we get the declaration string of an enum
in C++?
A big switch
is possible, but ugly.
The following is slightly better, but still very hard to maintain:
const char * get_LogLevel_str( LogLevel level)
{
struct {
LogLevel value;
const char * str;
} static s_all_log_levels[] = {
{ LogLevel::Info, "Info" },
{ LogLevel::Error, "Error"},
};
for(int i = 0; i < _countof(s_all_log_levels); i++)
{
if(s_all_log_levels[i].value == level)
return s_all_log_levels[i].str;
}
return NULL;
}
Someday when I want to add a Warning
enumerator to LogLevel
, most likely I'll forget to add it here. This violates the DRY (Don't Repeat Yourself) rule.
After Googling, I found the following article by the VC++ IDE team developer Rocky:
I was excited at the first look, and then used these tricks in our projects. Then I found that this solution was impractical because a non-trivial project will involve many enum definitions. Defining all of these enums in a separate header file makes it hard to use and hard to maintain.
A variation of the above solution is to dismiss the header file, and to use macros instead:
#define ALL_THE_WEEK \
ENUM_ITEM(Sunday)\
ENUM_ITEM(Monday)
#define ENUM_ITEM(a) a,
enum WeekDay { ALL_THE_WEEK };
#define ENUM_ITEM(a) {a, #a},
struct {
WeekDay val;
const char * str;
} static all_WeekDay_meta_data[] = {ALL_THE_WEEK};
Developers can focus on the definition of the ALL_THE_WEEK
macro and forget other code. This idea finally turned into the template + macro solution, which provides the C#-style enum reflection ability.
Using the Code
To use the code, you need:
#include "Reflection4CppEnum.hpp"
Define a macro to collect all of the enumerators, such as the above.
#define ALL_THE_WEEK(x, y) \
ENUM_ITEM(x, y, Sunday) \
ENUM_ITEM(x, y, Monday) \
ENUM_ITEM_VALUE(x, y, Tuesday, 25) \
ENUM_ITEM(x, y, Wednesday) \
ENUM_ITEM_VALUE(x, y, Thursday, 36) \
ENUM_ITEM(x, y, Friday) \
ENUM_ITEM(x, y, Saturday)
In fact, there are three macro names involved in this step: ALL_THE_WEEK
, ENUM_ITEM
, ENUM_ITEM_VALUE
.
Define the Reflection-ready enum:
DEF_ENUM(, WeekDay, ALL_THE_WEEK)
It's a little weird that the first parameter to the macros are left empty. Note: it's not a typo! It's for the namespace, and in this case, it's in the global namespace. For the global namespace or the default namespace, it must be left empty.
ENUM_ITEM, ENUM_ITEM_VALUE, DEF_ENUM
are macros provided by this utility component, the name is fixed.
It's your duty to name the macro ALL_THE_WEEK
and provide the macro definition to make it work with the solution. This work is always the same as above. Compared with the typical enum definition:
enum WeekDay { Sunday, Monday, Tuesday = 25,
Wednesday, Thursday = 36, Friday, Saturday, };
Note that there are two unavoidable extra macro parameters "x
" and "y
", which look useless at the first look. But it's vital to the solution, I'll explain it later. The other inconvenience (yet no workaround to my knowledge) is the ENUM_ITEM_VALUE
macro in place of ENUM_ITEM
to define the enum item with an explicit value!
WeekDay
is the type name for the enum.
It's ready to use the operation provided by EnumHelper<WeekDay>::xxx()
.
Get the enum name and the number of enumerators:
printf("%d enumerators defined for Enum [%s]\n", EnumHelper<WeekDay>::count(),
EnumHelper<WeekDay>::get_type_name() );
Iterate all the names and the corresponding values:
EnumHelper<WeekDay>::const_str_iterator it_str = EnumHelper<WeekDay>::str_begin();
EnumHelper<WeekDay>::const_value_iterator it_val = EnumHelper<WeekDay>::value_begin();
for(; it_str != EnumHelper<WeekDay>::str_end() && it_val !=
EnumHelper<WeekDay>::value_end();
++it_str, ++it_val)
{
printf("Enum str: [%20s], value: %d\n", *it_str, *it_val);
}
Determine whether an integer value is a defined enumerator:
int test_enum_val = 0;
printf("Is %d a defined enumerator for %s: %s\n", test_enum_val,
EnumHelper<WeekDay>::get_type_name(),
EnumHelper<WeekDay>::is_defined(test_enum_val)? "True" : "False" );
Determine whether a string value is a defined enumerator:
const char * test_enum_str = "Sunday";
printf("Is %s a defined enumerator for %s: %s(case-sensitive)\n", test_enum_str,
EnumHelper<WeekDay>::get_type_name(),
EnumHelper<WeekDay>::is_defined(test_enum_str) ? "True": "False" );
Support the case-insensitive is_defined
:
test_enum_str = "sunday";
printf("Is %s a defined enumerator for %s: %s(case-insensitive)\n", test_enum_str,
EnumHelper<WeekDay>::get_type_name(),
EnumHelper<WeekDay>::is_defined(test_enum_str, true) ? "True": "False" );
From enumerator to string:
printf("enum string for %d is %s\n", test_enum_val, EnumHelper<WeekDay>::to_string(0) );
printf("enum string for %d is %s\n", Sunday, EnumHelper<WeekDay>::to_string( Sunday ) );
printf("enum string for %d is %s\n", 104, EnumHelper<WeekDay>::to_string(
static_cast<WeekDay>(104) ) );
printf("enum string for %d is %s\n", 3, EnumHelper<Color>::bitflags_to_string(
static_cast<WeekDay>(3) ) );
From string to enumerator:
printf("enum value for %s is %d(case-insensitive)\n"
, test_enum_str, EnumHelper<WeekDay>::parse( test_enum_str, true ) );
try {
EnumHelper<WeekDay>::parse( test_enum_str, false );
} catch( runtime_error & e) {
fprintf(stderr, "exception: %s\n", e.what() );
}
test_enum_str = "Sunday";
printf("enum value for %s is %d(case-sensitive)\n", test_enum_str,
EnumHelper<WeekDay>::parse( test_enum_str) );
printf("enum string [Blue, Red] is %d\n", (int) EnumHelper<Color>::bitflags_parse(
"Blue, Red" ) );
Find the index for an enumerator value:
printf("enum value %d is the %d-th item in the declaration order\n",
0, EnumHelper<WeekDay>::index_of(0) );
printf("enum value %d is the %d-th item in the declaration order\n",
104, EnumHelper<WeekDay>::index_of(104) );
Find the index for an enumerator string:
printf("enum string [%s] is the %d-th item in the declaration order\n", test_enum_str,
EnumHelper<WeekDay>::index_of(test_enum_str) );
test_enum_str = "not exist";
printf("enum string [%s] is the %d-th item in the declaration order\n", test_enum_str,
EnumHelper<WeekDay>::index_of(test_enum_str) );
Points of Interest
The above ALL_THE_WEEK
is defined as a function style macro, while its use is variable style. By doing this, we can expand the macro with different parameters; this is why the "x
" and "y
" parameters exist!
#define DEF_ENUM(ns_cls, enum_type, list) \
DEF_ENUM_ONLY_(enum_type, list(1, ns_cls) ) \
REGISTER_ENUM_META_DATA_(ns_cls, enum_type, list(2, ns_cls) )
The macro ending with "_
" is intended for internal use only:
#define DEF_ENUM_ONLY_(enum_type, enum_list ) enum enum_type { enum_list };
#define ENUM_ITEM_VALUE_1_(ns_cls, enum_entry, enum_value) enum_entry = enum_value,
#define ENUM_ITEM_1_(ns_cls, enum_entry) enum_entry ,
#define ENUM_ITEM_VALUE(_12, ns_cls, enum_entry, enum_value) \
ENUM_ITEM_VALUE_##_12##_(ns_cls, enum_entry, enum_value)
#define ENUM_ITEM(_12, ns_cls, enum_entry) ENUM_ITEM_##_12##_(ns_cls, enum_entry)
So the macro list(1, ns_cls
) will be expanded to ALL_THE_WEEK(1, ns_cls)
in the first pass. Then, ALL_THE_WEEK(1, ns_cls)
is expanded to a list of ENUM_ITEM(1, ns_cls, Sunday)
, ENUM_ITEM_VALUE(1, ns_cls, Thursday, 25)
; in turn, ENUM_ITEM(1, ns_cls, Sunday)
expands to ENUM_ITEM_1_(ns_cls, Sunday)
, ENUM_ITEM_VALUE(1, Thursday, 25)
expands to ENUM_ITEM_VALUE_1_(ns_cls, Thursday, 25)
in the third pass. Finally, ENUM_ITEM_1
and ENUM_ITEM_VALUE_1_
macro expand to list of:
Sunday, Tuesday, Wednesday, Thursday = 25... etc
This is the enumerator list of a C/C++ enum definition.
For the template class to work, it's necessary to "save" the enum's value and its string representation in an enum-specific template class instance. That's what the REGISTER_ENUM_META_DATA_
macro and ENUM_ITEM_2_
(and the alike) macros do.
Let Easy Things Easy and Hard Things Possible
The above enum operation assumes that enums are defined in the file scope.
To define it inside a namespace scope, a two-stage definition must be employed:
#define ALL_THE_WEEK(x, y) \
ENUM_ITEM_VALUE(x, y, Sunday, 1) \
ENUM_ITEM_VALUE(x, y, Monday, 3) \
ENUM_ITEM_VALUE(x, y, Tuesday, Sunday) \
ENUM_ITEM_VALUE(x, y, Wednesday, 10) \
ENUM_ITEM_VALUE(x, y, Thursday, 7) \
ENUM_ITEM(x, y, Friday ) \
ENUM_ITEM_VALUE(x, y, Saturday, 12)
namespace C {
DEF_ENUM_1(C, WeekDay, ALL_THE_WEEK)
};
DEF_ENUM_2(C, WeekDay, ALL_THE_WEEK)
It's necessary to define the enum itself inside the class C
and instantiate the template class outside the class. It's inevitable because C++ does not allow specifying the initializer for arbitrary static data inside a class. By practice, I think DEF_ENUM_1
and DEF_ENUM_2
are easier to remember than others.
It's also possible to define the enum inside an anonymous namespace:
namespace {
DEF_ENUM_1(, WeekDay, ALL_THE_WEEK)
}
DEF_ENUM_2(, WeekDay, ALL_THE_WEEK)
Defining the enum inside a named namespace:
namespace F {
...
DEF_ENUM_1(F, WeekDay, ALL_THE_WEEK)
...
}
DEF_ENUM_2(F, WeekDay, ALL_THE_WEEK)
Defining the enum inside namespace and class:
namespace F {
class C {
private:
DEF_ENUM_1(F::C, WeekDay, ALL_THE_WEEK)
friend class EnumHelper<weekday>;
}
}
DEF_ENUM_2(F::C, WeekDay, ALL_THE_WEEK)
Note that inside the class, private enum
is allowed, but to make the enum accessible to EnumHelper
, a friend
declaration is needed. I chose to not include the friend
declaration inside the macro because I want to keep the syntax consistent for the namespace and class to make things simple. A macro cannot distinguish the context around it. A friend
declaration is illegal in the namespace scope.
It's no exception that when defining an enum inside namespace/class, you need to access it with the appropriate prefix:
printf("[%s]\n", EnumHelper<F::C::WeekDay>::to_string(F::C::Sunday) );
Working with a Visual C/C++ Extension for Enum Definition
For Microsoft Visual C/C++, it's possible to specify an underlying type for the enum:
enum Color : short { Red, Blue };
You can use the following macro in the file scope:
DEF_ENUM_WITH_TYPE(Color , short, ALL_THE_COLORS)
DEF_ENUM(Color , ALL_THE_COLORS)
The following inside the namespace/class:
DEF_ENUM_WITH_TYPE_1(namespace::class, Color , short, ALL_THE_COLORS)
DEF_ENUM_WITH_TYPE_2(namespace::class, Color , short, ALL_THE_COLORS)
instead of
DEF_ENUM_1(namespace::class, Color , ALL_THE_COLORS)
DEF_ENUM_2(namespace::class, Color , ALL_THE_COLORS)
For all the macros, the order of parameters from left to right is: namespace and class (if present), enum name, underlying type (if present), enumerator list.
Drawbacks
Unfortunately, I haven't figured out a way to support enums inside a function (and inside a class which in turn is inside a function) to work with this solution.
History
- 27 May, 2009: Initial post.
- 24 Sep, 2010: Uses macro tricks to improve usability, support enum inside namespace and class.