16-9 使用枚举器进行数组索引和长度操作

数组文档中一个较大的问题在于,整数索引无法向程序员提供关于索引含义的任何信息。

考虑一个存储5个测试分数的数组:

#include <vector>

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

    testScores[2] = 76; // who does this represent?
}

testScores[2]所代表的学生是谁?这点并不明确。


使用无作用域枚举进行索引

第16.3课——std::vector与无符号长度及下标问题中,我们花了大量时间讨论std::vector::operator[](以及其他可下标的C++容器类)的索引类型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; // we are now updating the test score belonging to stan

    return 0;
}

image

这样一来,数组中每个元素所代表的含义就清晰得多。

由于枚举类型隐式具有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 }; // non-constexpr

    testScores[name] = 76; // may trigger a sign conversion warning if Student::Names defaults to a signed underlying type

    return 0;
}

image

在此特定情况下,我们可以将名称设为常量表达式(constexpr),从而使从常量表达式有符号整数类型转换为std::size_t时不会发生类型缩减。然而,当初始化表达式不是常量表达式时,此方法将失效。

另一种方案是显式指定枚举的底层类型为无符号整数:

#include <vector>

namespace Students
{
    enum Names : unsigned int // explicitly specifies the underlying type is 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 }; // non-constexpr

    testScores[name] = 76; // not a sign conversion since name is unsigned

    return 0;
}

image

在上例中,由于 name 现在已保证是无符号整型,因此可以将其转换为 std::size_t 类型而不会出现符号转换问题。


使用计数枚举器

请注意,我们在枚举器列表末尾定义了一个额外的枚举器,名为 max_students。如果所有先前枚举器都使用默认值(推荐做法),则该枚举器的默认值将等于先前枚举器的总数。在上例中,由于之前定义了 5 个枚举器,因此 max_students 的值为 5。我们将其非正式地称为计数枚举器count enumerator,因其值代表先前定义枚举器的总数。

此计数枚举器可在需要获取先前枚举器数量的任何位置使用。例如:

#include <iostream>
#include <vector>

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

int main()
{
    std::vector<int> testScores(Students::max_students); // Create a vector with 5 elements

    testScores[Students::stan] = 76; // we are now updating the test score belonging to stan

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

    return 0;
}

image

我们在两个地方使用 max_students:首先,我们创建一个长度为 max_students 的 std::vector,因此该向量将为每位学生分配一个元素。我们还使用 max_students 来打印学生人数。

该技术还具备另一优势:若后续新增枚举项(位于 max_students 之前),则 max_students 将自动递增,所有使用该常量的数组将自动更新为新长度,无需额外修改。

#include <vector>
#include <iostream>

namespace Students
{
    enum Names
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        wendy, // 5 (added)
        // add future enumerators here
        max_students // now 6
    };
}

int main()
{
    std::vector<int> testScores(Students::max_students); // will now allocate 6 elements

    testScores[Students::stan] = 76; // still works

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

    return 0;
}

image


使用计数枚举器对数组长度进行断言

更常见的情况是,我们使用值初始化列表创建数组,目的是用枚举器对该数组进行索引。此时,验证容器大小是否等于枚举器数量很有必要。若该断言触发,则说明枚举器列表存在错误,或初始化值数量不匹配。当枚举类型新增枚举项时,若未相应更新数组初始化值,就容易发生这种情况。

例如:

#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 };

    // Ensure the number of test scores is the same as the number of students
    assert(std::size(testScores) == max_students);

    return 0;
}

image

提示:
如果数组是 constexpr 类型,则应改用 static_assert。std::vector 不支持 constexpr,但 std::array(以及 C 风格数组)支持。

我们将在第 17.3 课——传递和返回 std::array 中进一步讨论此内容。

最佳实践:
使用 static_assert 确保常量表达式数组的长度与计数枚举器匹配。

使用 assert 确保非常量表达式数组的长度与计数枚举器匹配。


数组与枚举类

由于无作用域枚举会以其枚举项污染定义所在的命名空间,当枚举尚未包含在其他作用域区域(如命名空间或类)中时,建议使用枚举类。

然而,由于枚举类不支持隐式转换为整数类型,当尝试将其枚举项用作数组索引时会遇到问题:

#include <iostream>
#include <vector>

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

int main()
{
    // compile error: no conversion from StudentNames to std::size_t
    std::vector<int> testScores(StudentNames::max_students);

    // compile error: no conversion from StudentNames to std::size_t
    testScores[StudentNames::stan] = 76;

    // compile error: no conversion from StudentNames to any type that operator<< can output
    std::cout << "The class has " << StudentNames::max_students << " students\n";

    return 0;
}

image

image

有几种方法可以解决这个问题。最明显的方法是将枚举器静态转换为整数:

#include <iostream>
#include <vector>

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

int main()
{
    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";

    return 0;
}

image

然而,这不仅输入起来很麻烦,还会让代码变得非常杂乱。

更好的选择是使用我们在第13.6节中介绍的辅助函数——带作用域的枚举(枚举类),它允许我们使用一元运算符+将枚举类的枚举器转换为整数值。

#include <iostream>
#include <type_traits> // for std::underlying_type_t
#include <vector>

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

// Overload the unary + operator to convert StudentNames to the underlying type
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";

    return 0;
}

image


然而,若需频繁进行枚举类型与整数类型的转换,建议直接在命名空间(或类)内使用标准枚举类型。

测验时间

问题 #1

创建一个程序定义的枚举类型(位于命名空间内),包含以下动物名称:鸡、狗、猫、大象、鸭子和蛇。定义一个数组,为每种动物分配一个元素,并使用初始化列表将每个元素初始化为该动物的腿数。验证数组初始化项数量正确。

编写 main() 函数,通过枚举器输出大象的腿数。

image

显示解决方案

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

namespace Animals
{
    enum Animals
    {
        chicken,
        dog,
        cat,
        elephant,
        duck,
        snake,
        max_animals
    };

    const std::vector legs{ 2, 4, 4, 4, 2, 0 };
}

int main()
{
    // Ensure the number of legs is the same as the number of animals
    assert(std::size(Animals::legs) == Animals::max_animals);

    std::cout << "An elephant has " << Animals::legs[Animals::elephant] << " legs.\n";

    return 0;
}
posted @ 2026-01-07 09:26  游翔  阅读(4)  评论(0)    收藏  举报