65.9K
CodeProject 正在变化。 阅读更多。
Home

枚举列表和枚举数组

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (9投票s)

2011 年 7 月 1 日

CPOL

11分钟阅读

viewsIcon

61532

downloadIcon

478

在枚举和数组之间建立牢固的绑定

目录

引言

在我的工作中,我经常遇到以下情况。在一个头文件中声明了一个枚举,比如说

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_mapboost::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!");

由于我声明 WeekDayNamesconst,因此无法使用 operator[]。但是,以下调用将起作用

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

让我们看看我们现在在哪里

  • 枚举量及其值紧密耦合,因此枚举量的顺序及其整数表示不会干扰结果。
  • 无法将另一个枚举的整数或枚举量传递给 std::map::find(...)
  • 枚举和数组的大小必须匹配才能编译代码。
  • 然而,如果枚举范围从 0 开始,连续(即 0123、...)且以终止枚举量(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 的工作区和项目文件

枚举数组使用的 namespacelobster::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_1static_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
© . All rights reserved.