Table of Contents
Introduction
During my work, I often came around the following situation. There is an enumeration 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 - as it is only needed at local scope, the enumeration is mapped to an array of arbitrary type. In the following example, a string
array is used as this happens often enough:
const char* const sWeekDay[] =
{
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
};
The enumerators' string
representations may be easily accessed, e.g., by 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 another enumeration as index or to 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 developer 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,
"array size mismatch!");
This does not look nice, but it would inform about sizing mismatches at compile time.
- A
std::map
initialized from a range of std::pair
s 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 in the source file, the following static
assertion can be placed:
static_assert(sizeof(WeekDayNames) / sizeof(WeekDayNames[0]) == DAY_LAST,
"array size mismatch!");
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 representations 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 array 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 template metaprogramming (TMP) 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".
While the original version of this article was stand-alone, this revised version is based on the templates provided in my CodeProject article Static Value Lists which serves as implementation base for the enumerator lists discussed in the following.
This article does not intend to explain the solution approach used but is an introduction to and reference for the provided templates. The technical intricacies of static
value lists are discussed in detail in the referred article - in comparison, the code introduced here is self-explaining.
Some special terminology is used (Please also refer to the "Static Value Lists" article):
- Enumerator Lists
A static
value list that is created on base of a series of enumerators from one enumeration. Later on, this type can be queried for information about itself. As already hinted, enumerator lists are static
value lists that contain enumerators of a certain enumeration. - Static Index Array
An array that is indexed by items from a static
value list. As a result, the static
index array has exactly as many elements as the static
value list has values. Values and array items are closely linked as the value index of in the static
value list is used as item index in the static
index array. - Enumerated Arrays
A static
index array that is created on base of an enumerator list. The values behind the enumerators may be accessed at run- and compile-time.
Using the Code
The provided templates are implemented in the lobster
project in header files only. It has the following directory structure:
lobster
: A header-only library providing a common namespace. Within the folder, header files are contained that can be externally referenced. E.g. including "static_list.h" will load all files required for working with static
lists. Therefore this folder is named as additional include folder in the sample code.
static_index_array
: The array template namespace that will be used to define enumerated arrays in the followingstatic_list
: The namespace providing the templates for implementing static
value lists and enumerator lists...
: Other namespaces that are not discussed here
enm_array_sample
source
: Contains the sample code files gcc
: Batch file for compiling the sample with gcc. The output will be an executable in the same foldermsvc
: Workspace and project files for VS2010
The namespace
used for enumerated arrays is lobster::static_index_array
, it has to be loaded by including "static_index_array.h" from the "lobster" folder. It is intended to provide a using
directive to the lobster
namespace and then use static_index_array
as prefix to the templates explicitly. Otherwise, there may be naming collisions with STL or other code. The sample code however sets the using
directive directly.
The coding convention in the lobster
library 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 and tested in VC++ 2010 and GCC 4.5.2, but may work on other C++ compilers providing the TR1 extensions as well (I have not tested this).
The sample project contains the "enm_demo.cpp" file providing the examples discussed here.
Enumerator Lists
For defining enumerator lists, the lobster
library "static_list.h" header has to be included (Make sure that the folder "lobster" is referenced appropriately):
#include "static_list.h"
An enumerator list is declared in the same way as any other static
value list. The most primitive approach is by using the static_list::list_item
template. This is a recursive template so for each enumerator, one static_list::list_item
definition 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 static_list::list_tail
template, it defines the enumeration used and must be the provider of all enumerators.
typedef list_item<list_item<list_item<list_tail<EWeekDay>,
DAY_MONDAY>, DAY_TUESDAY>, DAY_WEDNESDAY> my_day_list;
As this is rather cumbersome, there are helper templates static_list::list_1
to static_list::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<list_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 static
value list offers the following run-time functionality:
list_item<...> Member
| Description |
static int index_of(value_type) | The index of an enumerator in the list is returned (Due to the template instantiation, value_type will be defined as EWeekDay in the previous example. If the enumerator is not part of the list, -1 is returned. |
static enum_type at(int) | The value at the provided index is returned. If the index is invalid, a std::out_of_range exception is thrown. |
list_item<...>::const_iterator | An input iterator for the enumerator list is provided. As the values are defined at compile time, only const-access to its values is possible. An example for using the iterator is given in "enm_demo.cpp" when calling the iterator_test(...) function. |
static list<...>::const_iterator begin() | An iterator pointing to the first item in the list is returned. |
static list<...>::const_iterator end() | An iterator pointing behind the last item in the list is returned. |
At compile-time
, the template class static_list::value_index_of<list, enumerator>::value
can be used to query the index of a value (enumerator in this case) in the list. If the value 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 static
value lists are referred in "static_index_array.h"
.
#include "static_index_array.h"
A static
index array can be seen as a enumerated array generalization where not only enumerator lists but also other static
value lists may serve as index provider.
An enumerated array is defined by using the lobster::static_index_array::array
class template. The static
value list defining the enumerator list is simply passed as a template parameter:
typedef array<weekdays, std::string> weekday_string_array;
Aside from the standard constructor, there are two other 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 passed directly:
weekday_string_array wsa(WeekDayNames);
In this variant, the parameter must be a C-style array of std::pair<value_type, any_type>
.
The static_index_array::array
template is const
-correct, so it is possible to use the const
-qualifier in the declaration. This will prevent the contained values to be modified later on.
static_index_array::array
provides the following methods:
static_index_array::array<...> Methods:
| Description |
value_type& value<key_type>();
const value_type& value<key_type>() const; | Access is granted to the value associated with an enumerator (key_type is defined as the proper enumeration). The enumerator is evaluated at compile-time. If the enumerator is not part of this static_index_array::array 's enumerator list, the method template will not compile. However, the compiler error may not be very helpful. |
value_type& value(key_type);
const value_type& value(key_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 static_index_array::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 static_index_array::defaulting_array
template class is used as an adapter for the enumerated array. It provides the same implicit interface (see table above) as static_index_array::array
and can be used as drop-in replacement.
typedef defaulting_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 initialization 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);
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.
TMP code is certainly delays compile-time is not easily optimized for run-time. efficiency. However I don't think that this is a problem as the devices offered here are supposedly only useful for small enumerations.
The initializing static_index_array::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 TMP 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 TMP 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/10/03 Version 2
- Update for VS2010 and GCC 4.5.2
- 2011/06/30 Version 1