在之前的课程(无作用域枚举的整数转换)中,我们展示了如下示例:
#include <iostream>
enum Color
{
black, // 0
red, // 1
blue, // 2
};
int main()
{
Color shirt{ blue };
std::cout << "Your shirt is " << shirt << '\n';
return 0;
}
该程序的输出为:
Your shirt is 2
由于 operator<<
不知道如何打印一个 Color
类型,编译器会隐式地将 Color
转换为整数值并打印该整数值。
大多数情况下,将枚举类型以整数值(如 2)的形式打印出来并不是我们所期望的。相反,我们通常希望打印出枚举器所代表的名称(例如 blue
)。C++ 并没有提供直接将枚举类型转换为字符串的功能,因此我们需要自己寻找解决方案。幸运的是,这并不困难。
获取枚举器的名称
获取枚举器名称的典型方法是编写一个函数,允许我们传入一个枚举器,并返回该枚举器的名称作为字符串。但这需要某种方式来确定对于给定的枚举器应该返回哪个字符串。
有两种常见的方法可以实现这一点。
在第 8.5 课 – switch
语句基础中,我们提到 switch
语句可以基于整数值或枚举值进行分支选择。在以下示例中,我们使用 switch
语句选择枚举器,并返回与该枚举器对应的适当颜色字符串字面量:
#include <iostream>
#include <string_view>
enum Color
{
black,
red,
blue,
};
constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case black: return "black";
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
int main()
{
constexpr Color shirt{ blue };
std::cout << "Your shirt is " << getColorName(shirt) << '\n';
return 0;
}
该程序的输出为:
Your shirt is blue
在上述示例中,我们基于 color
进行分支选择,color
是我们传入的枚举器。在 switch
语句中,我们为 Color
的每个枚举器都提供了一个 case
标签。每个 case
返回对应颜色的名称作为 C 风格字符串字面量。该 C 风格字符串字面量会被隐式地转换为 std::string_view
,并返回给调用者。我们还提供了一个 default
情况,用于返回 "???"
,以防用户传入我们未预期的值。
提醒:
由于 C 风格字符串字面量在整个程序的生命周期内都存在,因此返回一个查看 C 风格字符串字面量的 std::string_view
是可以的。当 std::string_view
被复制回调用者时,被查看的 C 风格字符串字面量仍然存在。
该函数被声明为 constexpr
,因此我们可以在常量表达式中使用颜色的名称。
相关内容:
constexpr
函数将在第 F.1 课 – constexpr
函数中介绍。
虽然这种方法可以让我们获取枚举器的名称,但如果想将该名称打印到控制台,使用 std::cout << getColorName(shirt)
并不如直接使用 std::cout << shirt
那样简洁。我们将在后续的第 13.5 课 – 输入输出运算符的重载介绍中,教 std::cout
如何打印枚举类型。
将枚举器映射为字符串的第二种方法是使用数组。我们将在第 17.6 课 – std::array
和枚举类型中介绍这种方法。
无作用域枚举的输入
现在,我们来看一个输入的示例。在以下示例中,我们定义了一个 Pet
枚举类型。由于 Pet
是用户自定义的类型,语言并不知道如何使用 std::cin
输入一个 Pet
类型:
#include <iostream>
enum Pet
{
cat, // 0
dog, // 1
pig, // 2
whale, // 3
};
int main()
{
Pet pet { pig };
std::cin >> pet; // 编译错误:std::cin 不知道如何输入一个 Pet
return 0;
}
解决这个问题的一个简单方法是读取一个整数,然后使用 static_cast
将该整数转换为相应枚举类型的枚举器:
#include <iostream>
#include <string_view>
enum Pet
{
cat, // 0
dog, // 1
pig, // 2
whale, // 3
};
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 "???";
}
}
int main()
{
std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";
int input{};
std::cin >> input; // 输入一个整数
if (input < 0 || input > 3)
std::cout << "You entered an invalid pet\n";
else
{
Pet pet{ static_cast<Pet>(input) }; // 将整数静态转换为 Pet
std::cout << "You entered: " << getPetName(pet) << '\n';
}
return 0;
}
虽然这种方法可以工作,但显得有些笨拙。另外请注意,只有在确定 input
的值在枚举器范围内时,我们才应该使用 static_cast<Pet>(input)
。
从字符串获取枚举类型
与其让用户输入一个数字,不如让用户输入一个代表枚举器的字符串(例如 "pig"
),然后我们将该字符串转换为相应的 Pet
枚举器。然而,实现这一点需要我们解决一些挑战。
首先,我们不能基于字符串使用 switch
语句,因此我们需要使用其他方法来匹配用户传入的字符串。这里最简单的办法是使用一系列的 if
语句。
其次,如果用户传入了一个无效的字符串,我们应该返回哪个 Pet
枚举器呢?一种选择是添加一个表示“无/无效”的枚举器,并返回它。然而,更好的选择是使用 std::optional
。
相关内容:
我们将在第 12.15 课 – std::optional
中介绍 std::optional
。
#include <iostream>
#include <optional> // 用于 std::optional
#include <string>
#include <string_view>
enum Pet
{
cat, // 0
dog, // 1
pig, // 2
whale, // 3
};
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)
{
// 我们只能基于整数值(或枚举类型)使用 switch 语句,而不能基于字符串
// 因此这里我们需要使用 if 语句
if (sv == "cat") return cat;
if (sv == "dog") return dog;
if (sv == "pig") return pig;
if (sv == "whale") return whale;
return {}; // 表示“无值”
}
int main()
{
std::cout << "Enter a pet: cat, dog, pig, or whale: ";
std::string s{};
std::cin >> s;
std::optional<Pet> pet { getPetFromString(s) };
if (!pet)
std::cout << "You entered an invalid pet\n";
else
std::cout << "You entered: " << getPetName(*pet) << '\n';
return 0;
}
在上述解决方案中,我们使用一系列的 if-else
语句进行字符串比较。如果用户的输入字符串与某个枚举器的字符串匹配,我们就返回相应的枚举器。如果没有任何字符串匹配,我们就返回 {}
,表示“无值”。
对于高级读者:
请注意,上述解决方案只匹配小写字