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

.NET-like Reflection Support for Enums in C++

4.08/5 (8 votes)
27 Sep 2010CPOL6 min read 35.3K   150  
This article provides a macro + template solution to support .NET-like Reflection for enums such as ToString, IsDefined, Parse, GetValues, GetNames.

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:

C++
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:

C++
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:

C++
#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:

C++
#include "Reflection4CppEnum.hpp"

Define a macro to collect all of the enumerators, such as the above.

C++
#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:

C++
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:

C++
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:

C++
// .NET equivalent: typeof(WeekDay).Length       Enum.GetNames(typeof(WeekDay)).Name
printf("%d enumerators defined for Enum [%s]\n", EnumHelper<WeekDay>::count(),
      EnumHelper<WeekDay>::get_type_name() );

Iterate all the names and the corresponding values:

C++
// .NET equivalent:
//      foreach(string s in Enum.GetNames(typeof(WeekDay)) ) Console.WriteLine( s );
//      foreach(object o in Enum.GetValues(typeof(WeekDay))) Console.WriteLine( o );
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:

C++
// .NET equivalent:
//     Console.WriteLine(  Enum.IsDefined(typeof(WeekDay), 0) );
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:

C++
// .NET equivalent:
//     Console.WriteLine(  Enum.IsDefined(typeof(WeekDay), "Sunday" ) );
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:

C++
// .NET equivalent: No
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:

C++
// .NET equivalent:
//     WeekDay day = Sunday; Console.WriteLine( day.ToString() );
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 ) );
//     for a non-exist item, get digital representation
printf("enum string for %d is %s\n",  104, EnumHelper<WeekDay>::to_string(
    static_cast<WeekDay>(104) ) );

//     for a bit flags enum, returns comma-separated list
//     enum Color { Blue = 1, Red = 2 }   3 result  Blue, Red
printf("enum string for %d is %s\n",  3, EnumHelper<Color>::bitflags_to_string(
    static_cast<WeekDay>(3) ) );

From string to enumerator:

C++
// .NET equivalent:
//     WeekDay day = Enum.Parse( typeof(WeekDay), "Sunday");
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) );

//     for a bit flags enum, parse a comma-separated list
//     enum Color { Blue = 1, Red = 2 }   parse  "Blue, Red" result 3
printf("enum string [Blue, Red] is %d\n", (int) EnumHelper<Color>::bitflags_parse(
    "Blue, Red" ) );

Find the index for an enumerator value:

C++
// .NET equivalent: No
printf("enum value %d is the %d-th item in the declaration order\n", 
    0, EnumHelper<WeekDay>::index_of(0) );
//     return -1 for the non exist value
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:

C++
// .NET equivalent: No
printf("enum string [%s] is the %d-th item in the declaration order\n", test_enum_str,
    EnumHelper<WeekDay>::index_of(test_enum_str) );
//     return -1 for the non exist strings:
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!

C++
#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:

C++
#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:

C++
#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:

C++
namespace {
DEF_ENUM_1(, WeekDay, ALL_THE_WEEK)
}
DEF_ENUM_2(, WeekDay, ALL_THE_WEEK)

Defining the enum inside a named namespace:

C++
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:

C++
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:

C++
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:

C++
enum Color : short { Red, Blue };

You can use the following macro in the file scope:

C++
DEF_ENUM_WITH_TYPE(Color , short, ALL_THE_COLORS)
//instead of
DEF_ENUM(Color , ALL_THE_COLORS)

The following inside the namespace/class:

C++
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)