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
由于无作用域枚举会隐式转换为 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;
}

这样一来,数组中每个元素所代表的含义就清晰得多。
由于枚举类型隐式具有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;
}

在此特定情况下,我们可以将名称设为常量表达式(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;
}

在上例中,由于 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;
}

我们在两个地方使用 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;
}

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

提示:
如果数组是 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;
}


有几种方法可以解决这个问题。最明显的方法是将枚举器静态转换为整数:
#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;
}

然而,这不仅输入起来很麻烦,还会让代码变得非常杂乱。
更好的选择是使用我们在第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;
}

然而,若需频繁进行枚举类型与整数类型的转换,建议直接在命名空间(或类)内使用标准枚举类型。
测验时间
问题 #1
创建一个程序定义的枚举类型(位于命名空间内),包含以下动物名称:鸡、狗、猫、大象、鸭子和蛇。定义一个数组,为每种动物分配一个元素,并使用初始化列表将每个元素初始化为该动物的腿数。验证数组初始化项数量正确。
编写 main() 函数,通过枚举器输出大象的腿数。

显示解决方案
#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;
}

浙公网安备 33010602011771号