当使用 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.
...
