数组的一个显著文档缺陷是:整型下标无法向程序员传达任何关于索引含义的信息。
示例:学生测验成绩
假设我们有一个包含 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_assert
。std::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;
}
解决方式:
- 显式
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";
- 重载一元
+
运算符(更简洁)——课程 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
创建自定义枚举(置于命名空间内),包含以下动物名称:chicken
、dog
、cat
、elephant
、duck
、snake
。
定义一个数组,为每种动物分配一个元素,并用初始化列表初始化该数组,使其保存每种动物的腿数。断言数组初始化器数量正确。
在 main()
中打印大象的腿数,使用枚举器。
[显示解答]