章节目录

std::array和枚举

本节阅读量:

在之前,我们讨论过,可以使用枚举作为数组的长度和索引。

既然已经了解过constexpr std::array,接下来继续讨论并展示一些额外的技巧。


使用静态断言确保适当数量的数组初始值设定项

当使用CTAD初始化constexpr std::array时,编译器将根据初始值设定项的数量推断数组的长度。如果提供的初始值设定项少于应有的数量,则数组将比预期的短,并且访问它可能导致未定义的行为。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#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 }; // oops, 只有 4 个元素

    std::cout << "Cartman got a score of " << testScores[StudentNames::cartman] << '\n'; // 无效的索引,导致未定义的行为

    return 0;
}

如果需要检查constexpr std::array中的初始值设定项数量时,可以使用静态断言执行此操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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 };

    // 确保学生都有一个分数
    static_assert(std::size(testScores) == max_students); // 编译失败: static_assert 不满足条件

    std::cout << "Cartman got a score of " << testScores[StudentNames::cartman] << '\n';

    return 0;
}

这样,如果稍后添加新的枚举器,但忘记向testScores添加相应的初始值设定项,则程序将无法编译。

您还可以使用静态断言来确保两个不同的constexpr std::array具有相同的长度。


使用constexpr数组实现更好的枚举输入和输出

在之前的I/O操作符重载简介中,我们介绍了输入和输出枚举器名称的几种方法。为了帮助完成这项任务,有了将枚举转换为字符串的辅助函数,反之亦然。这些函数都有自己的字符串文本集合,必须专门编码逻辑来检查每个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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 {};
}

这意味着如果要添加新的枚举元素,必须记住更新这些函数。

让我们稍微改进一下这些函数。在枚举器的值从0开始并按顺序继续的情况下(这对于大多数枚举都是正确的),可以使用array来保存每个枚举器的名称。

这允许我们做两件事:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <array>
#include <iostream>
#include <string>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // 使用 sv 后缀, 这样 std::array 中保存的元素类型为 std::string_view
    using namespace std::string_view_literals; // 引入 sv 后缀
    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[color];
}

// 设置 operator<< 如何输出 Color
// std::ostream 是 std::cout 的类型
// 返回值和参数都是引用 (避免制作额外的副本)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

// 设置 operator>> 如何输入 Color
// 传递 non-const 引用,以便修改 color的值
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;
        }
    }

    // 如果没找到,已经是输入不对
    // 将输入 stream 设置成 fail 状态
    in.setstate(std::ios_base::failbit);

    // 提取失败, operator>> 对于基础类型,会返回0初始化的数据
    // 注释掉下面一行,对于 color 会执行同样的逻辑
    // color = {};
    return in;
}

int main()
{
    auto 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';

    return 0;
}

这将打印:

1
2
3
Your shirt is blue
Enter a new color: red
Your shirt is now red

基于范围的循环和枚举

有时,我们会遇到这样的情况,即迭代枚举元素是有用的。虽然可以使用具有整数索引的for循环来实现这一点,但这可能需要将整数索引静态强制转换为枚举类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <array>
#include <iostream>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // 使用 sv 后缀, 这样 std::array 中保存的元素类型为 std::string_view
    using namespace std::string_view_literals; // for sv suffix
    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[color];
}

// 设置 operator<< 如何输出 Color
// std::ostream 是 std::cout 的类型
// 返回值和参数都是引用 (避免制作额外的副本)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

int main()
{
    // 使用for 循环去遍历所有的color
    for (int i=0; i < Color::max_colors; ++i )
        std::cout << static_cast<Color::Type>(i) << '\n';

    return 0;
}

不幸的是,基于范围的for循环不允许迭代枚举的枚举元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <array>
#include <iostream>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // 使用 sv 后缀, 这样 std::array 中保存的元素类型为 std::string_view
    using namespace std::string_view_literals; // for sv suffix
    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[color];
}

// 设置 operator<< 如何输出 Color
// std::ostream 是 std::cout 的类型
// 返回值和参数都是引用 (避免制作额外的副本)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

int main()
{
    for (auto c: Color::Type) // 编译失败: 不能遍历枚举
        std::cout << c < '\n';

    return 0;
}

有许多创造性的解决方案。由于我们可以在数组上使用基于范围的for循环,因此最简单的解决方案之一是创建一个包含每个枚举元素的constexpr std::array,然后对其进行迭代。此方法仅在枚举元素具有唯一值时有效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <array>
#include <iostream>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,     // 0
        red,       // 1
        blue,      // 2
        max_colors // 3
    };

    using namespace std::string_view_literals; // for sv suffix
    constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };
    static_assert(std::size(colorName) == max_colors);

    constexpr std::array types { black, red, blue }; // 一个 std::array 包含所有的枚举元素
    static_assert(std::size(types) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    return Color::colorName[color];
}

// 设置 operator<< 如何输出 Color
// std::ostream 是 std::cout 的类型
// 返回值和参数都是引用 (避免制作额外的副本)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

int main()
{
    for (auto c: Color::types) // ok: 可以在 std::array 使用基于范围的 for 循环
        std::cout << c << '\n';

    return 0;
}

在上面的示例中,由于Color::types的元素类型是Color::type,因此变量c将被推导为Color::type,这正是我们想要的!

这将打印:

1
2
3
black
red
blue

17.4 通过std::reference_wrapper创建引用的数组

上一节

17.6 C样式数组简介

下一节