std::array 与枚举:编译期校验、输入输出优化及遍历技巧

当使用 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 内:

  1. 定义枚举 Typechicken, dog, cat, elephant, duck, snake
  2. 定义结构体 Datastd::string_view name, int legs, std::string_view sound
  3. 创建 constexpr std::array<Data>,为每种动物填入数据
  4. 读入动物名称,若存在则打印信息,再打印其余动物信息;若不存在,打印提示后打印全部动物信息

示例交互:

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.
...

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"