17-6 std::array 和枚举(todo)
在第16.9节——使用枚举器进行数组索引和长度操作中,我们讨论了数组和枚举。
现在我们的工具箱中已拥有constexpr 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 }; // oops, only 4 values
std::cout << "Cartman got a score of " << testScores[StudentNames::cartman] << '\n'; // undefined behavior due to invalid index
return 0;
}

当常量表达式 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 };
// Ensure the number of test scores is the same as the number of students
static_assert(std::size(testScores) == max_students); // compile error: static_assert condition failed
std::cout << "Cartman got a score of " << testScores[StudentNames::cartman] << '\n';
return 0;
}

这样,如果后续添加了新的枚举类型却忘记在 testScores 中添加对应的初始化器,程序将无法编译通过。
你也可以使用静态断言来确保两个不同的 constexpr std::array 数组具有相同的长度。
使用 constexpr 数组实现更优的枚举输入输出
在第13.5节——输入输出运算符重载入门中,我们探讨了若干枚举名称的输入输出方法。为辅助此任务,我们设计了辅助函数将枚举值转换为字符串并实现反向转换。这些函数各自拥有独立(且重复)的字符串常量集,我们不得不编写专门逻辑来逐个验证:
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开始并按顺序递增(大多数枚举都符合此条件)时,我们可以使用数组来存储每个枚举项的名称。
这样能实现两点:
- 通过枚举值索引数组获取对应枚举名称;
- 通过循环遍历所有名称,并能根据索引将名称关联回对应枚举值。
#include <array>
#include <iostream>
#include <string>
#include <string_view>
namespace Color
{
enum Type
{
black,
red,
blue,
max_colors
};
// use sv suffix so std::array will infer type as std::string_view
using namespace std::string_view_literals; // for sv suffix
constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };
// Make sure we've defined strings for all our colors
static_assert(std::size(colorName) == max_colors);
};
constexpr std::string_view getColorName(Color::Type color)
{
// We can index the array using the enumerator to get the name of the enumerator
return Color::colorName[static_cast<std::size_t>(color)];
}
// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
return out << getColorName(color);
}
// Teach operator>> how to input a Color by name
// We pass color by non-const reference so we can have the function modify its value
std::istream& operator>> (std::istream& in, Color::Type& color)
{
std::string input {};
std::getline(in >> std::ws, input);
// Iterate through the list of names to see if we can find a matching name
for (std::size_t index=0; index < Color::colorName.size(); ++index)
{
if (input == Color::colorName[index])
{
// If we found a matching name, we can get the enumerator value based on its index
color = static_cast<Color::Type>(index);
return in;
}
}
// We didn't find a match, so input must have been invalid
// so we will set input stream to fail state
in.setstate(std::ios_base::failbit);
// On an extraction failure, operator>> zero-initializes fundamental types
// Uncomment the following line to make this operator do the same thing
// 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;
}
这将输出:

基于范围的 for 循环与枚举
有时我们会遇到需要遍历枚举类型枚举器的场景。虽然可以使用带整数索引的 for 循环实现,但这往往需要大量将整数索引静态转换为枚举类型的操作。
#include <array>
#include <iostream>
#include <string_view>
namespace Color
{
enum Type
{
black,
red,
blue,
max_colors
};
// use sv suffix so std::array will infer type as std::string_view
using namespace std::string_view_literals; // for sv suffix
constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };
// Make sure we've defined strings for all our colors
static_assert(std::size(colorName) == max_colors);
};
constexpr std::string_view getColorName(Color::Type color)
{
return Color::colorName[color];
}
// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
return out << getColorName(color);
}
int main()
{
// Use a for loop to iterate through all our colors
for (int i=0; i < Color::max_colors; ++i )
std::cout << static_cast<Color::Type>(i) << '\n';
return 0;
}

遗憾的是,基于范围的 for 循环无法让你遍历枚举的枚举器:
#include <array>
#include <iostream>
#include <string_view>
namespace Color
{
enum Type
{
black,
red,
blue,
max_colors
};
// use sv suffix so std::array will infer type as std::string_view
using namespace std::string_view_literals; // for sv suffix
constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };
// Make sure we've defined strings for all our colors
static_assert(std::size(colorName) == max_colors);
};
constexpr std::string_view getColorName(Color::Type color)
{
return Color::colorName[color];
}
// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
return out << getColorName(color);
}
int main()
{
for (auto c: Color::Type) // compile error: can't traverse enumeration
std::cout << c < '\n';
return 0;
}

对此存在多种创新解决方案。由于我们可以对数组使用基于范围的 for 循环,最直接的方法之一是创建一个包含所有枚举项的 constexpr std::array,然后对其进行迭代。此方法仅在枚举项具有唯一值时有效。
#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 }; // A std::array containing all our enumerators
static_assert(std::size(types) == max_colors);
};
constexpr std::string_view getColorName(Color::Type color)
{
return Color::colorName[color];
}
// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
return out << getColorName(color);
}
int main()
{
for (auto c: Color::types) // ok: we can do a range-based for on a std::array
std::cout << c << '\n';
return 0;
}
在上例中,由于 Color::types 的元素类型是 Color::Type,变量 c 将被推断为 Color::Type,这正是我们想要的结果!
这将输出:

测验时间
定义一个名为Animal的命名空间。在其内部,定义一个包含以下动物的枚举类型:鸡(chicken)、狗(dog)、猫(cat)、大象(elephant)、鸭子(duck)和蛇(snake)。同时创建一个名为Data的结构体,用于存储每种动物的名称、腿数及其发出的声音。创建一个Data类型的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.
A cat has 4 legs and says meow.
A elephant has 4 legs and says pawoo.
A duck has 2 legs and says quack.
A snake has 0 legs and says hissss.
Enter an animal: frog
That animal couldn't be found.
Here is the data for the rest of the animals:
A chicken has 2 legs and says cluck.
A dog has 4 legs and says woof.
A cat has 4 legs and says meow.
A elephant has 4 legs and says pawoo.
A duck has 2 legs and says quack.
A snake has 0 legs and says hissss.
问题 #1
显示解答
#include <array>
#include <iostream>
#include <string>
#include <string_view>
namespace Animal
{
enum Type
{
chicken,
dog,
cat,
elephant,
duck,
snake,
max_animals
};
struct Data
{
std::string_view name{};
int legs{};
std::string_view sound{};
};
constexpr std::array types { chicken, dog, cat, elephant, duck, snake };
constexpr std::array data {
Data{ "chicken", 2, "cluck" },
Data{ "dog", 4, "woof" },
Data{ "cat", 4, "meow" },
Data{ "elephant", 4, "pawoo" },
Data{ "duck", 2, "quack" },
Data{ "snake", 0, "hissss" },
};
static_assert(std::size(types) == max_animals);
static_assert(std::size(data) == max_animals);
}
// Teach operator>> how to input an Animal by name
// We pass animal by non-const reference so we can have the function modify its value
std::istream& operator>> (std::istream& in, Animal::Type& animal)
{
std::string input {};
std::getline(in >> std::ws, input);
// See if we can find a match
for (std::size_t index=0; index < Animal::data.size(); ++index)
{
if (input == Animal::data[index].name)
{
animal = static_cast<Animal::Type>(index);
return in;
}
}
// We didn't find a match, so input must have been invalid
// so we will set input stream to fail state
in.setstate(std::ios_base::failbit);
return in;
}
void printAnimalData(Animal::Type type)
{
const Animal::Data& animal { Animal::data[type] };
std::cout << "A " << animal.name << " has " << animal.legs << " legs and says " << animal.sound << ".\n";
}
int main()
{
std::cout << "Enter an animal: ";
Animal::Type type{};
std::cin >> type;
// If users input didn't match
if (!std::cin)
{
std::cin.clear();
std::cout << "That animal couldn't be found.\n";
type = Animal::max_animals; // set to invalid option so we don't match below
}
else
printAnimalData(type);
std::cout << "\nHere is the data for the rest of the animals:\n";
for (auto a : Animal::types)
{
if (a != type)
printAnimalData(a);
}
return 0;
}

浙公网安备 33010602011771号