枚举列表和枚举数组






4.60/5 (9投票s)
在枚举和数组之间建立牢固的绑定
目录
引言
在我的工作中,我经常遇到以下情况。在一个头文件中声明了一个枚举,比如说
enum EWeekDay {
DAY_SUNDAY,
DAY_MONDAY,
DAY_TUESDAY,
DAY_WEDNESDAY,
DAY_THURSDAY,
DAY_FRIDAY,
DAY_SATURDAY,
DAY_LAST
};
在另一个代码位置,通常是一个源文件 - 因为它只在局部作用域需要,枚举被映射到一个任意类型的数组。在下面的例子中,一个 string
数组被使用,因为这种情况发生得足够频繁
const char* const sWeekDay[] =
{
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
};
枚举量的 string
表示很容易访问,例如,通过编写 sWeekDay[DAY_TUESDAY]
。不幸的是,这种方法完全不安全,因为它不会在 EWeekDay
发生变化时强制更新 sWeekDay
- 如果从枚举中添加或删除了枚举量,则不强制更新数组。这将在添加枚举量时导致数组越界问题,在删除枚举量时导致枚举量/
string
不匹配。 - 交换枚举量(例如,将
DAY_SUNDAY
放在一周的末尾)将导致枚举量/string
不匹配。 - 代表枚举量的整数值必须是一个连续的范围,否则将无法转换为数组索引。
此外,数组与枚举没有直接关联。可以将另一个枚举的枚举量作为索引传递,或直接使用整数。
确实,可以采取一些简单的步骤来提高枚举数组的安全性
- 可以使用代码注释来告知开发人员其他需要更改的代码位置。
- 数组可以声明为
const char* const sWeekDay[DAY_LAST]
。这不会告知程序员他将不得不更改数组,但至少它会自动调整数组大小并用0
初始化缺失的值。然后,运行时代码可以断言0
指针。 - 可以使用静态断言来验证枚举的长度与数组的长度是否匹配
static_assert(sizeof(sWeekDay) / sizeof(sWeekDay[0]) == DAY_LAST, "array size mismatch!");
这看起来不太好看,但它会在编译时告知大小不匹配。
- 可以使用一个从
std::pair
范围初始化的std::map
来组合枚举和数组(如下所示)。 - 另一个选择是使用
boost::unordered_map
和boost::unordered_map::assign::map_list_of
。 - 使用
enum2str<...>
模板,可能还有十几种解决方案。
使用 std::map
的方法是
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]);
在源文件的某个位置,可以放置以下 static
断言
static_assert(sizeof(WeekDayNames) / sizeof(WeekDayNames[0]) == DAY_LAST,
"array size mismatch!");
由于我声明 WeekDayNames
为 const
,因此无法使用 operator[]
。但是,以下调用将起作用
std::cout << DayMap.find(DAY_TUESDAY)->second;
让我们看看我们现在在哪里
- 枚举量及其值紧密耦合,因此枚举量的顺序及其整数表示不会干扰结果。
- 无法将另一个枚举的整数或枚举量传递给
std::map::find(...)
。 - 枚举和数组的大小必须匹配才能编译代码。
- 然而,如果枚举范围从
0
开始,连续(即0
、1
、2
、3
、...)且以终止枚举量(DAY_LAST
)结束,那么保护 map 大小的static
断言才有效。
在此基础上,我考虑了一种方法来进一步提高使用安全性,方法是将潜在问题从运行时异常转移到编译时错误,并为枚举和枚举数组引入一些新功能。当然,我很快就使用了模板元编程 (TMP)。
背景
我的工作部分基于 Andrei Alexandrescu 的书《Modern C++ Design》的第 3 章“Typelists”,其中讨论了类型列表和元组的思想和实现。关于迭代器实现的信息来自 Bjarne Stroustrup 的书《The C++ Programming Language》的“第 19 章 迭代器和分配器”。
虽然本文的原始版本是独立的,但这个修改版本基于我在 CodeProject 文章 Static Value Lists 中提供的模板,该模板作为下面讨论的枚举列表的实现基础。
本文不打算解释所使用的解决方案方法,而是作为所提供模板的介绍和参考。static
值列表的技术细节在引用的文章中有详细讨论 - 相比之下,这里介绍的代码是自解释的。
使用了一些特殊术语(也请参考“Static Value Lists”文章)
- 枚举列表
一个static
值列表,它是基于一个枚举的多个枚举量创建的。稍后,可以查询此类型关于自身的信息。正如已经暗示的那样,枚举列表是包含特定枚举的枚举量的static
值列表。 - 静态索引数组
一个由static
值列表项索引的数组。结果是,static
索引数组的元素数量与static
值列表的值数量完全相同。值和数组项紧密关联,因为static
值列表中的值索引被用作static
索引数组中的项索引。 - 枚举数组
一个基于枚举列表创建的static
索引数组。枚举量背后的值可以在运行时和编译时访问。
使用代码
提供的模板在 lobster
项目中以头文件形式实现。它有以下目录结构
lobster
: 一个仅头文件的库,提供一个公共命名空间。在文件夹内,包含可以外部引用的头文件。例如,包含“static_list.h”将加载使用static
列表所需的所有文件。因此,该文件夹在示例代码中被命名为附加包含文件夹。static_index_array
: 下面将用于定义枚举数组的数组模板命名空间static_list
: 提供实现static
值列表和枚举列表的模板的命名空间...
: 其他未在此讨论的命名空间
enm_array_sample
source
: 包含示例代码文件gcc
: 使用 gcc 编译示例的批处理文件。输出将在同一文件夹中生成一个可执行文件msvc
: VS2010 的工作区和项目文件
枚举数组使用的 namespace
是 lobster::static_index_array
,它必须通过包含 "lobster"
文件夹中的 "static_index_array.h" 来加载。建议使用 using
指令指向 lobster
命名空间,然后显式地使用 static_index_array
作为模板的前缀。否则,可能会与 STL 或其他代码发生命名冲突。然而,示例代码直接设置了 using
指令。
lobster
库中的编码约定尽可能基于 STL,但是示例代码没有应用任何编码约定。
该库和示例代码是在 VC++ 2010 和 GCC 4.5.2 中编写和测试的,但可能在其他提供 TR1 扩展的 C++ 编译器上也能工作(我没有测试过)。
示例项目包含“enm_demo.cpp”文件,提供了此处讨论的示例。
枚举列表
要定义枚举列表,必须包含 lobster
库的 “static_list.h” 头文件(确保“lobster”文件夹被正确引用)
#include "static_list.h"
枚举列表的声明方式与其他 static
值列表相同。最基本的方法是使用 static_list::list_item
模板。这是一个递归模板,因此每个枚举量都使用一个 static_list::list_item
定义。枚举量的值可以显式设置,只要它们对每个条目都是唯一的即可。
枚举列表的尾部必须由 static_list::list_tail
模板显式设置,它定义了使用的枚举,并且必须是所有枚举量的提供者。
typedef list_item<list_item<list_item<list_tail<EWeekDay>,
DAY_MONDAY>, DAY_TUESDAY>, DAY_WEDNESDAY> my_day_list;
由于这相当繁琐,因此有辅助模板 static_list::list_1
到 static_list::list_10
,可以更轻松地构造列表。但是,尾部必须手动设置为第一个模板参数,因为可以链接枚举列表定义。
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;
static
值列表提供以下 **运行时** 功能
list_item<...> 成员 |
描述 |
static int index_of(value_type) |
返回列表中枚举量的索引(由于模板实例化,在前面的示例中,value_type 将被定义为 EWeekDay 。如果枚举量不是列表的一部分,则返回 -1 。 |
static enum_type at(int) |
返回给定索引处的值。如果索引无效,则抛出 std::out_of_range 异常。 |
list_item<...>::const_iterator |
提供枚举列表的输入迭代器。由于值是在编译时定义的,因此只能对其值进行 const 访问。在 “enm_demo.cpp” 中调用 iterator_test(...) 函数时,会给出使用迭代器的示例。 |
static list<...>::const_iterator begin() |
返回指向列表中第一个项的迭代器。 |
static list<...>::const_iterator end() |
返回指向列表中最后一项之后的迭代器。 |
在 **编译时**,可以使用模板类 static_list::value_index_of<list, enumerator>::value
查询列表中值的索引(在这种情况下是枚举量)。如果值不是列表的一部分,则返回 -1
。它在编译时测试枚举量是否存在于列表中时非常有用。
枚举数组
在 "static_index_array.h"
中引用了实现枚举数组和 static
值列表所需的文件。
#include "static_index_array.h"
静态索引数组可以看作是枚举数组的泛化,其中不仅枚举列表,还可以使用其他 static
值列表作为索引提供者。
枚举数组是通过使用 lobster::static_index_array::array
类模板定义的。定义枚举列表的 static
值列表仅作为模板参数传递。
typedef array<weekdays, std::string> weekday_string_array;
除了标准构造函数外,还有另外两个构造函数可用于初始化枚举数组。
提供了一个类似于 template <class InputIterator> map(InputIterator first, InputIterator last ...)
的构造函数,以便以与 std::map
示例相同的方式进行初始化。
weekday_string_array wsa(&WeekDayNames[0], &WeekDayNames[DAY_LAST])
请注意,解引用输入迭代器必须可以访问一个 std::pair
,其第一个值必须是所用枚举的枚举量,其第二个类型必须可以隐式转换为枚举数组的值类型。在这种情况下,C 风格的 string
被转换为 std::string
。
或者,可以直接传递 std::pair
数组。
weekday_string_array wsa(WeekDayNames);
在此变体中,参数必须是 std::pair<value_type, any_type>
的 C 风格数组。
static_index_array::array
模板是 const
-correct 的,因此可以在声明中使用 const
限定符。这将防止后续修改其中的值。
static_index_array::array
提供了以下方法
static_index_array::array<...> 方法 |
描述 |
value_type& value<key_type>(); const value_type& value<key_type>() const; |
可以访问与枚举量关联的值(key_type 定义为正确的枚举)。枚举量在 **编译时** 进行求值。如果枚举量不是此 static_index_array::array 的枚举列表的一部分,则方法模板将无法编译。但是,编译器错误可能不是很有帮助。 |
value_type& value(key_type); const value_type& value(key_type) const; |
可以访问与枚举量关联的值。枚举量在 **运行时** 进行求值,因此如果枚举量不是此 static_index_array::array 的枚举列表的一部分,则会抛出异常。 |
枚举数组的默认值
可以改用默认值作为后备,而不是导致编译时或运行时错误。为此,使用 static_index_array::defaulting_array
模板类作为枚举数组的适配器。它提供了与 static_index_array::array
相同的隐式接口(参见上表),并且可以作为即插即用替换。
typedef defaulting_array<weekdays, std::string> weekday_default_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);
总结
枚举数组构造提供了以下优点
- 枚举不需要提供连续值的枚举量。
- 枚举量及其值是紧密耦合的。
- 可以从一个枚举生成多个枚举列表。
- 在使用数组构造函数时,枚举的大小和
WeekDayNames
必须匹配才能编译代码。 - 提供了对枚举数组的编译时访问,因此错误的枚举量将导致编译器错误。
- 也提供了运行时访问,因此要访问的值字段也可以在运行时确定。
TMP 代码无疑会延迟编译时间,并且不容易为运行时效率进行优化。但我认为这不成问题,因为这里提供的设备据称只适用于小型枚举。
初始化 static_index_array::array
的构造函数不检查所提供对的完整性。通过提供相同的枚举量两次来欺骗它是有可能的。
值得关注的点
我开始这个项目是我第一次尝试大规模使用 TMP。结果发现它非常耗时且难以调试(更不用说由于我的错误或编译器而导致的编译器崩溃了)。
TMP 的功能非常强大,在基础库开发中起着重要作用,但我不会鼓励在应用程序开发中使用它。基于模板的组件也难以文档化,因为它们不提供清晰的接口。
历史
- 2011/10/03 版本 2
- 更新支持 VS2010 和 GCC 4.5.2
- 2011/06/30 版本 1