C++ 使用枚举器进行数组索引与长度管理

数组的一个显著文档缺陷是:整型下标无法向程序员传达任何关于索引含义的信息

示例:学生测验成绩

假设我们有一个包含 5 个学生成绩的数组:

#include <vector>

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };

    testScores[2] = 76; // 这代表哪位学生?
}

testScores[2] 代表哪位学生?并不清晰。


使用非作用域枚举器作为索引

在课程 16.3《std::vector 与无符号长度及下标问题》中,我们详细讨论了 std::vector<T>::operator[] 的下标类型为 size_type,通常是 std::size_t 的别名。因此,下标必须是 std::size_t 或可转换为 std::size_t 的类型。

由于非作用域枚举会隐式转换为 std::size_t,我们可以用非作用域枚举器作为数组下标,从而文档化索引含义

#include <vector>

namespace Students
{
    enum Names
    {
        kenny,   // 0
        kyle,    // 1
        stan,    // 2
        butters, // 3
        cartman, // 4
        max_students // 5
    };
}

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };

    testScores[Students::stan] = 76; // 明确更新 stan 的成绩

    return 0;
}

如此,各数组元素所代表的学生一目了然。

由于枚举器隐式为 constexpr,其向无符号整型的转换不被视为收缩转换,从而避免符号/无符号索引问题。


使用非 constexpr 的非作用域枚举器索引

非作用域枚举的底层类型由实现定义,可能是带符号或无符号整型。由于枚举器隐式为 constexpr,只要我们仅用枚举器索引,就不会出现符号转换警告。

然而,若定义一个非常量的枚举变量,并以其索引 std::vector,则当枚举底层类型默认为带符号时,可能触发符号转换警告:

#include <vector>

namespace Students
{
    enum Names
    {
        kenny,   // 0
        kyle,    // 1
        stan,    // 2
        butters, // 3
        cartman, // 4
        max_students // 5
    };
}

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };
    Students::Names name { Students::stan }; // 非常量

    testScores[name] = 76; // 若底层类型为带符号,可能触发符号转换警告

    return 0;
}

在此例中,可将 name 设为 constexpr(从而转换安全),但若初始化表达式非 constexpr,则无法如此。

另一种方案是显式指定底层类型为无符号整型

#include <vector>

namespace Students
{
    enum Names : unsigned int // 显式指定底层类型
    {
        kenny,   // 0
        kyle,    // 1
        stan,    // 2
        butters, // 3
        cartman, // 4
        max_students // 5
    };
}

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };
    Students::Names name { Students::stan }; // 非常量

    testScores[name] = 76; // 无符号,无符号转换问题

    return 0;
}

使用计数枚举器

注意我们在枚举列表末尾额外定义了 max_students。若前面所有枚举器均使用默认值(推荐做法),则 max_students 的值等于前面枚举器的个数。上例中 max_students 为 5,即前面定义了 5 个枚举器。我们称这种枚举器为计数枚举器(count enumerator)。

该计数枚举器可用于任何需要“前面枚举器个数”的场合:

#include <iostream>
#include <vector>

namespace Students
{
    enum Names
    {
        kenny,   // 0
        kyle,    // 1
        stan,    // 2
        butters, // 3
        cartman, // 4
        // 后续枚举器可继续添加
        max_students // 5
    };
}

int main()
{
    std::vector<int> testScores(Students::max_students); // 创建含 5 个元素的 vector

    testScores[Students::stan] = 76; // 更新 stan 的成绩

    std::cout << "The class has " << Students::max_students << " students\n";

    return 0;
}

此技巧的好处是:若后续再添加枚举器(放在 max_students 之前),max_students 会自动加 1,所有使用 max_students 的数组长度也会自动更新,无需额外修改。

#include <vector>
#include <iostream>

namespace Students
{
    enum Names
    {
        kenny,   // 0
        kyle,    // 1
        stan,    // 2
        butters, // 3
        cartman, // 4
        wendy,   // 5(新增)
        // 后续枚举器
        max_students // 现为 6
    };
}

int main()
{
    std::vector<int> testScores(Students::max_students); // 自动分配 6 个元素

    testScores[Students::stan] = 76; // 仍有效

    std::cout << "The class has " << Students::max_students << " students\n";

    return 0;
}

用计数枚举器断言数组长度

更常见的情形是用初始化列表创建数组,并打算用枚举器索引。此时,可用断言确保容器大小与计数枚举器一致。若断言触发,说明枚举列表或初始化列表有误。这在新增枚举器却忘记添加对应初始值时极易发生。

示例:

#include <cassert>
#include <iostream>
#include <vector>

enum StudentNames
{
    kenny,   // 0
    kyle,    // 1
    stan,    // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };

    // 确保成绩数量与学生数量一致
    assert(std::size(testScores) == max_students);

    return 0;
}

提示
若数组为 constexpr,应改用 static_assertstd::vector 不支持 constexpr,但 std::array(及 C 风格数组)支持。

相关内容见课程 17.3《传递与返回 std::array》。

最佳实践

  • constexpr 数组,用 static_assert 确保长度与计数枚举器匹配。
  • 对非 constexpr 数组,用 assert 确保长度与计数枚举器匹配。

数组与 enum class

由于非作用域枚举会将其枚举器暴露到所在命名空间,若枚举未置于其他作用域(如命名空间或类),推荐使用 enum class

然而,enum class 不会隐式转换为整型,因此无法直接用作数组下标:

#include <iostream>
#include <vector>

enum class StudentNames // 改为 enum class
{
    kenny,   // 0
    kyle,    // 1
    stan,    // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    // 编译错误:无法从 StudentNames 转换到 std::size_t
    std::vector<int> testScores(StudentNames::max_students);

    // 编译错误:无法从 StudentNames 转换到 std::size_t
    testScores[StudentNames::stan] = 76;

    // 编译错误:无法转换到 operator<< 能输出的类型
    std::cout << "The class has " << StudentNames::max_students << " students\n";

    return 0;
}

解决方式:

  1. 显式 static_cast(冗长):
std::vector<int> testScores(static_cast<int>(StudentNames::max_students));
testScores[static_cast<int>(StudentNames::stan)] = 76;
std::cout << "The class has " << static_cast<int>(StudentNames::max_students) << " students\n";
  1. 重载一元 + 运算符(更简洁)——课程 13.6 已介绍:
#include <iostream>
#include <type_traits>
#include <vector>

enum class StudentNames
{
    kenny, kyle, stan, butters, cartman, max_students
};

// 重载一元 +,将枚举器转换为其底层类型
constexpr auto operator+(StudentNames a) noexcept
{
    return static_cast<std::underlying_type_t<StudentNames>>(a);
}

int main()
{
    std::vector<int> testScores(+StudentNames::max_students);
    testScores[+StudentNames::stan] = 76;
    std::cout << "The class has " << +StudentNames::max_students << " students\n";
}

若需大量枚举器到整型的转换,更简单做法是在命名空间(或类)内使用普通枚举。


小测验

问题 1
创建自定义枚举(置于命名空间内),包含以下动物名称:chickendogcatelephantducksnake
定义一个数组,为每种动物分配一个元素,并用初始化列表初始化该数组,使其保存每种动物的腿数。断言数组初始化器数量正确。
main() 中打印大象的腿数,使用枚举器。

[显示解答]

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"