Introduction

During my work, I often came around the following situation. There is an enumeration probably declared in a header file, let's say:

enum EWeekDay {
    DAY_SUNDAY,
    DAY_MONDAY,
    DAY_TUESDAY,
    DAY_WEDNESDAY,
    DAY_THURSDAY,
    DAY_FRIDAY,
    DAY_SATURDAY,
    DAY_LAST
};

In another code location, typically a source file, e.g., because it is only needed at local scope, the enumeration is mapped to an array of arbitrary type. In this sample, it is a string array as this happens often enough:

const char* const sWeekDay[] = 
{
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday"
};

Now the string representations may be easily accessed by e.g. writing sWeekDay[DAY_TUESDAY]. Unfortunately, this approach is entirely unsafe as it does not enforce updates of sWeekDay when changes occur to EWeekDay:

  • If enumerators are added or removed from the enumeration, the update of the array is not enforced. This will result in index out of bounds problems when an enumerator is added and enumerator / string mismatches when one is removed.
  • Swapping enumerators (e.g. placing DAY_SUNDAY at the end of the week) will result in enumerator / string mismatches.
  • The integral values representing the enumerator must be a continuous range, otherwise the conversion to array indexes will not be possible.

Besides that, the array is not directly linked to the enumeration. It would be possible to pass an enumerator of a wrong enumeration as index or directly use integers.

It is true that very simple steps can be taken to improve the safety of the enumerated array:

  • Code comments can be used to tell the programmer about other code locations that need changing.
  • The array may be declared as const char* const sWeekDay[DAY_LAST]. This would not inform the programmer that he will have to change the array, but at least it would automatically size the array and initialize missing values with 0. Runtime-code then could assert on 0-pointers.
  • A static assertion may be used to verify the length of the enumeration against the length of the array:
    static_assert<sizeof(sWeekDay) / sizeof(sWeekDay[0]) == DAY_LAST>();

    This does not look nice, but it would inform about sizing mismatches at compile time.

  • A std::map initialized from a range of std::pairs may be used to combine enumeration and array (shown below).
  • Another option is to use boost::unordered_map and boost::unordered_map::assign::map_list_of.
  • Using a enum2str<...> template and probably a dozen more solutions.

The approach using std::map would be:

std::pair<EWeekDay, const char* const> WeekDayNames[] = 
{
    std::make_pair(DAY_SUNDAY,      "Sunday"),
    std::make_pair(DAY_MONDAY,      "Monday"),
    std::make_pair(DAY_TUESDAY,     "Tuesday"),
    std::make_pair(DAY_WEDNESDAY,   "Wednesday"),
    std::make_pair(DAY_THURSDAY,    "Thursday"),
    std::make_pair(DAY_FRIDAY,      "Friday"),
    std::make_pair(DAY_SATURDAY,    "Saturday")
};

const std::map<EWeekDay, const char* const> DayMap(&WeekDayNames[0], 
	&WeekDayNames[DAY_LAST]);

Somewhere at function scope, the static assertion can be placed:

static_assert<sizeof(WeekDayNames) / sizeof(WeekDayNames[0]) == DAY_LAST>();

As I declared WeekDayNames as const, it is not possible to use operator[]. However the following call will work:

std::cout << DayMap.find(DAY_TUESDAY)->second;

Let's see where we are:

  • The enumerators and their values are tightly coupled so the order of enumerators and their integer representation will not interfere with the result.
  • It is not possible to pass an integer or enumerator of another enumeration to std::map::find(...).
  • The size of the enumeration and WeekDayNames must match for the code to compile.
  • Still, the static assertion safeguarding the map size will only work if the enumerated range starts at 0, runs continuously (i.e., 0, 1, 2, 3, ...) and ends with a terminating enumerator (DAY_LAST).

Starting from this, I thought about a way to improve usage safety further by moving potential problems from run-time exceptions to compile-time errors and also introduce some new features to enumerations and enumerated arrays. Of course, I came to use templates very quickly.

Background

My work is partly based on the Andrei Alexandrescu's book "Modern C++ Design", chapter 3 "Typelists" where the idea and implementation of type lists and tuples is discussed. Information about iterator implementation is taken from Bjarne Stroustrup's book, "The C++ Programming Language", "Chapter 19 Iterators and Allocators".

This article does not intend to explain the solution approach used but is an introduction to and reference for the provided templates. If there is a need for it, I will add a description of the techniques used later on.

Using the Code

The provided templates are implemented in the EnumeratorLib project in header files only. There are two segments for the following feature sets:

  • Enumerator Lists
    A type is created on base of a series of enumerators from one enumeration. Later on, this type can be queried for information about itself.
  • Enumerated Arrays
    A type is created on base of an enumerator list which represents an array which uses the enumerators in the enumerator list as indexes. The values behind these indexes may be accessed at run- and compile-time.

The namespace used in EnumeratorLib is lobster::enm. It is intended to provide a using directive to the lobster namespace and then use enm as prefix to the templates explicitly. Otherwise, there may be namespace collisions with STL or other code. The sample code however sets the using directive directly to lobster::enm.

The coding convention in EnumeratorLib is based on the STL as much as possible to me, however no coding convention is applied to the sample code.

The library and sample code is written in VC++ 2008, but it should work on other C++ compilers providing the TR1 extensions as well (I have not tested this).

The sample project contains the "EnumeratedObjects.cpp" file providing the examples discussed here.

Enumerator Lists

For using enumerator lists, the "enm_list.h" header has to be included:

#include "enm_list.h"

An enumerator list is declared from an enumeration by using the enm::list template. This is a "recursive" template so for each enumerator, one enm::list is used. The enumerator values can be set explicitly which is ok as long as they are individual for each entry.

The enumerator list tail must be explicitly set by the enm::tail template, it defines the enumeration used.

typedef list<list<list<tail<EWeekDay>, 
DAY_MONDAY>, DAY_TUESDAY>, DAY_WEDNESDAY> my_day_list;

As this is rather cumbersome, there are helper templates enm::list_1 to enm::list_10 which allow to construct the lists more easily. Still, the tail must be set as first template parameter manually as it is possible to chain enumerator list definitions:

typedef list_5<tail<EWeekDay>, DAY_MONDAY, DAY_TUESDAY, 
DAY_WEDNESDAY, DAY_THURSDAY, DAY_FRIDAY>::value workdays;
typedef list_2<workdays, DAY_SATURDAY, DAY_SUNDAY>::value weekdays;

The list offers the following run-time functionality:

list<...> Member

Description
static int index_of(enum_type)
The index of an enumerator in the list is returned. If the enumerator is not part of the enumerator list, -1 is returned.
static enum_type at(int)
The enumerator at the provided index is returned. If the index is invalid, a std::out_of_range exception is thrown.
list<...>::const_iterator
An input iterator for the enumerator list is provided. As the enumerated values are defined at compile time, only const-access to its values is possible. An example for using the iterator is given in "EnumeratedObjects.cpp".
static list<...>::const_iterator begin()
An iterator pointing to the first enumerator in the list is returned.
static list<...>::const_iterator end()
An iterator pointing behind the last enumerator in the list is returned.

At compile-time, the template class index_of<list, enumerator>::value can be used to query the index of an enumerator in the list. If the enumerator is not part of the list, -1 is returned. It is mostly helpful when testing for the presence of an enumerator in a list at compile-time.

Enumerated Arrays

The files required for implementing enumerated arrays as well as enumerator lists are referred in "enm_array.h".

#include <code>"enm_array.h"

An enumerated array is defined by using the enm::array class template. The enumerator list is simply wrapped by the enumeration array:

typedef array<weekdays, std::string> weekday_string_array;

Aside from the standard constructor, there are two constructors available for initializing the enumerated array.

A constructor similar to template <class InputIterator> map (InputIterator first, InputIterator last ...) is provided to allow initialization in the same way as it is done in the std::map sample:

weekday_string_array wsa(&WeekDayNames[0], &WeekDayNames[DAY_LAST])

Please note that dereferencing the input iterator must give access to a std::pair whose first value must be an enumerator of the enumeration used and whose second type must be implicitly convertible to the enumerated array value type. In this case, C-style strings are converted into std::string.

Alternatively, the std::pair array can be used directly:

weekday_string_array wsa(WeekDayNames);

In this variant, the parameter must be a C-style array of std::pair<enum_type, any_type>.

The enm::array is const-correct, so it's possible to use the const-qualifier in the declaration. This will prevent the array value to be modified later on.

enm::array provides the following methods:

enm::array Method

Description
value_type& value<enum_type>();
const value_type& value<enum_type>() const;
Access is granted to the value associated with an enumerator. The enumerator is evaluated at compile-time. If the enumerator is not part of this enm::array's enumerator list, the method template will not compile. However, the compiler error will not be very helpful.
value_type& value(enum_type);
const value_type& value(enum_type) const;
Access is granted to the value associated with an enumerator. The enumerator is evaluated at run-time, so an exception will be thrown if the enumerator is not part of this enm::array's enumerator list.

Default Values for Enumerated Arrays

Instead of causing compile- or run-time errors, it is possible to use a default value as fallback. For this, the enm::default_value template class is used as wrapper around the enumerated array. enm::default_value provides the same interface as enm::array and can be used as drop-in replacement.

typedef default_value<array<weekdays, std::string> > weekday_default_array;

The methods of this class will grant access to the default value if provided with an enumerator not in the enumerator list. The initialization of the default value is possible by extending the initializator array:

std::pair<EWeekDay, char const * const> WeekDayNames_Def[] = 
{
    std::make_pair(DAY_SUNDAY,      "Sunday"),
    std::make_pair(DAY_MONDAY,      "Monday"),
    std::make_pair(DAY_TUESDAY,     "Tuesday"),
    std::make_pair(DAY_WEDNESDAY,   "Wednesday"),
    std::make_pair(DAY_THURSDAY,    "Thursday"),
    std::make_pair(DAY_FRIDAY,      "Friday"),
    std::make_pair(DAY_SATURDAY,    "Saturday"),
    std::make_pair(DAY_LAST,        "#error")
};

const weekday_default_array wda(WeekDayNames_Def);

Unfortunately, I have trouble with the VC++ 2008 SP1 compiler and enm::default_value. A hack used by defining ENUMERATORLIB_VS2008HACK is provided in order to avoid the problem by modifying the class template. When using the hack, a second template parameter defining the enumeration must be passed - again.

// VS2008 Hack:
typedef default_value<array<weekdays, std::string>, EWeekDay> weekday_default_array;

I am not sure what the problem is, but the enm::default_value code looks ok to me. It would be interesting to know how other compilers react to the template class.

Summary

The enumerated array construct provides the following benefits:

  • The enumeration does not need to provide enumerators of continuous values.
  • The enumerators and their values are tightly coupled.
  • Multiple enumerator lists can be generated from one enumeration.
  • The size of the enumeration and WeekDayNames must match for the code to compile when using the array constructor.
  • Compile-time access to the enumerated array is granted, so that wrong enumerators will cause a compiler-error.
  • Run-time access is also granted so the value field which is to be accessed may be determined at run-time, too.

The EnumeratorLib is certainly not optimized for a short compile-time and also not run-time optimized. I don't think that this is a problem as the devices offered here are supposedly only useful for small enumerations.

The initializing enm::array constructors do not check the completeness of the provided pairs. It would be possible to trick it by providing the same enumerator twice.

Points of Interest

I started this project as my first attempt of using template metaprogramming in more than a minimalistic scale. It turned out to be time-consuming and hard to debug (not to mention the compiler crashes due to my mistakes or the compilers).

The things one can do with template metaprogramming are amazing and it plays an important role in basic library development, but it is not something I would encourage during application development. It is also hard to document components based on templates as they do not provide clear interfaces.

History

  • 2011/06/30 Version 1
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"