Enumerator Lists and Enumerated Arrays
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 with0
. Runtime-code then could assert on0
-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 ofstd::pair
s may be used to combine enumeration and array (shown below). - Another option is to use
boost::unordered_map
andboost::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 at0
, 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 string
s 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