当使用 CTAD 初始化 constexpr std::array
时,编译器会根据初始化器数量推断数组长度。若初始化器数量不足,数组将短于预期,索引时将出现未定义行为。
示例:
#include <array>
#include <iostream>
enum StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
int main()
{
constexpr std::array testScores{ 78, 94, 66, 77 }; // 只给了 4 个值
std::cout << "Cartman got a score of "
<< testScores[StudentNames::cartman] << '\n'; // 越界,UB
}
解决方法:在编译期用 static_assert
校验初始化器数量。
constexpr std::array testScores{ 78, 94, 66, 77, 14 }; // 正确 5 个值
static_assert(std::size(testScores) == max_students,
"初始化器数量应与枚举数量一致");
若后续新增枚举却忘记补初始化器,程序会直接编译失败,从而避免运行时错误。
利用 constexpr
数组改进枚举的输入输出
2.1 传统做法的痛点
我们曾手动实现枚举与字符串的双向转换:
constexpr std::string_view getPetName(Pet pet)
{
switch (pet)
{
case cat: return "cat";
case dog: return "dog";
case pig: return "pig";
case whale: return "whale";
default: return "???";
}
}
constexpr std::optional<Pet> getPetFromString(std::string_view sv)
{
if (sv == "cat") return cat;
if (sv == "dog") return dog;
if (sv == "pig") return pig;
if (sv == "whale") return whale;
return {};
}
缺点:
- 需维护两份重复的字符串字面量。
- 新增枚举时必须同时修改两处代码。
2.2 用数组保存枚举名称
当枚举值从 0 开始顺序递增(大多数情况如此),可把枚举名称放进 constexpr std::array
,实现:
- 通过枚举值索引数组得到名称;
- 通过遍历数组把名称映射回枚举值。
完整示例:
#include <array>
#include <iostream>
#include <string>
#include <string_view>
namespace Color
{
enum Type
{
black,
red,
blue,
max_colors
};
// 使用 sv 后缀让数组推导为 std::string_view
using namespace std::string_view_literals;
constexpr std::array colorName{ "black"sv, "red"sv, "blue"sv };
// 确保字符串数量与枚举一致
static_assert(std::size(colorName) == max_colors);
}
constexpr std::string_view getColorName(Color::Type color)
{
return Color::colorName[static_cast<std::size_t>(color)];
}
// 重载 << 使 Color::Type 可直接输出
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
return out << getColorName(color);
}
// 重载 >> 根据名称读入 Color::Type
std::istream& operator>>(std::istream& in, Color::Type& color)
{
std::string input;
std::getline(in >> std::ws, input);
for (std::size_t index = 0; index < Color::colorName.size(); ++index)
{
if (input == Color::colorName[index])
{
color = static_cast<Color::Type>(index);
return in;
}
}
in.setstate(std::ios_base::failbit);
return in;
}
int main()
{
Color::Type shirt{ Color::blue };
std::cout << "Your shirt is " << shirt << '\n';
std::cout << "Enter a new color: ";
std::cin >> shirt;
if (!std::cin)
std::cout << "Invalid\n";
else
std::cout << "Your shirt is now " << shirt << '\n';
}
运行结果:
Your shirt is blue
Enter a new color: red
Your shirt is now red
枚举遍历
3.1 问题:范围 for 不能直接遍历枚举
for (auto c : Color::Type) // 编译错误
std::cout << c << '\n';
3.2 解决方案:构造枚举数组
把枚举值放入 constexpr std::array
,即可用范围 for 遍历:
#include <array>
#include <iostream>
#include <string_view>
namespace Color
{
enum Type
{
black, red, blue, max_colors
};
using namespace std::string_view_literals;
constexpr std::array colorName{ "black"sv, "red"sv, "blue"sv };
static_assert(std::size(colorName) == max_colors);
constexpr std::array types{ black, red, blue }; // 包含所有枚举值
static_assert(std::size(types) == max_colors);
}
int main()
{
for (auto c : Color::types) // OK
std::cout << c << '\n';
}
输出:
black
red
blue
小测验
在命名空间 Animal
内:
- 定义枚举
Type
:chicken, dog, cat, elephant, duck, snake
- 定义结构体
Data
:std::string_view name, int legs, std::string_view sound
- 创建
constexpr std::array<Data>
,为每种动物填入数据 - 读入动物名称,若存在则打印信息,再打印其余动物信息;若不存在,打印提示后打印全部动物信息
示例交互:
Enter an animal: dog
A dog has 4 legs and says woof.
Here is the data for the rest of the animals:
A chicken has 2 legs and says cluck.
...